I'm trying to implement Onedrive client login by using Connect to identity providers with Web Account Manager
With this method finally I get a token using this code
private static async Task<string> RequestTokenAndSaveAccount(WebAccountProvider Provider, String Scope, String ClientID)
{
try
{
WebTokenRequest webTokenRequest = new WebTokenRequest(Provider, "wl.signin onedrive.appfolder onedrive.readwrite", ClientID);
WebTokenRequestResult webTokenRequestResult = await WebAuthenticationCoreManager.RequestTokenAsync(webTokenRequest);
if (webTokenRequestResult.ResponseStatus == WebTokenRequestStatus.Success)
{
App.settings.onedriveStoredAccountKey = webTokenRequestResult.ResponseData[0].WebAccount.Id;
return webTokenRequestResult.ResponseData[0].Token;
}
return "";
}
catch (Exception ex)
{
Debug.WriteLine(ex.ToString());
return "";
}
}
But I can't use the returned token to create a OnedriveClient because I need a MsaAuthenticationProvider to create the client and it creates its own token ignoring the one coming from the WebTokenRequest, and it doesn't have any method to take the prior token.
There is a way to create a OneDriveClient without going to REST Onedrive API?
Thank you
Edit:
As there are (at this time) two main versions of OneDriveSDK and those are different from each other, there are two ways to achieve this.
OneDrive.SDK 1.x
As #Brad said, an IAuthenticationProvider is needed to create the OneDriveClient.
I got the solution from https://github.com/ginach/Simple-IAuthenticationProvider-sample-for-OneDrive-SDK.
I took the SimpleAuthenticationProvider into my code, and then created the client like this
var client = new OneDriveClient(
new AppConfig(),
/* credentialCache */ null,
new Microsoft.OneDrive.Sdk.HttpProvider(),
new ServiceInfoProvider(new SimpleAuthenticationProvider { CurrentAccountSession = new Microsoft.OneDrive.Sdk.AccountSession { accessToken = AccessToken } }),
ClientType.Consumer);
client.BaseUrl = "https://api.onedrive.com/v1.0";
await client.AuthenticateAsync();
Where the accessToken is taken from the RequestTokenAndSaveAccount method.
OneDrive.SDK 2.x
For this case, the answer given by #dabox is the right solution.
Appending to Brad's answer, you can create a new AuthenticationProivder implements the IAuthenticationProivder interface in the package Microsoft.Graph.Core. And there also is a DelegateAuthenticationProvider in package Microsoft.Graph.Core which provides a Delegate interface for you. An example looks like:
OneDriveClient oneDriveClient = new OneDriveClient(
new DelegateAuthenticationProvider(
async (requestMessage) =>
{
string accessToken = await GetAccessTokenSomeWhereAsync();
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", accessToken);
}));
return oneDriveClient ;
Modified based on the Microsoft Graph's asp .net example: https://github.com/microsoftgraph/aspnet-connect-sample/blob/master/Microsoft%20Graph%20SDK%20ASPNET%20Connect/Microsoft%20Graph%20SDK%20ASPNET%20Connect/Helpers/SDKHelper.cs#L18
OneDriveClient only requires an IAuthenticationProvider, which is a pretty simplistic interface. You can create your own and implement AuthenticateRequestAsync such that it calls your RequestTokenAndSaveAccount and then adds the bearer token to the request.
Related
I am running an OAuth Dialog that allows user to sign in. I am looking to get this Auth token from DialogsClass.cs to my Bot.Cs class file and use it to make Graph calls.
I have tried to save token as string in local file within my dialog class and then read it back in main bot class but this solution does not seems as a right way of doing it.
AuthDialog.cs in Waterfall step:
var tokenResponse = (TokenResponse)stepContext.Result;
Expected result. Transfer this token from Dialog class to MainBot.cs class and use as string to make Graph calls.
Are you using one waterfall step to get token with OAuthPrompt and then another step to call a different class (in which you do graph api calls)?
Why can't you just pass the token to the down stream class?
If there are other steps in the middle, there are multiple ways to resolve it:
Use WaterfallStepContext Values
Save to your own UserState
Microsoft suggests not to store token in the system but make a call to oAuth prompt
return await stepContext.BeginDialogAsync(nameof(OAuthPrompt), null, cancellationToken);
and get latest token whenever you have to call Graph API. Once you receive the token in var tokenResponse = (TokenResponse)stepContext.Result;
you can make a call to GraphClient class which will create the Graph API client using the token in Authorization attribute.
var client = new GraphClientHelper(tokenResponse.Token);
Graph Client implementation:
public GraphClientHelper(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
throw new ArgumentNullException(nameof(token));
}
_token = token;
}
private GraphServiceClient GetAuthenticatedClient()
{
var graphClient = new GraphServiceClient(
new DelegateAuthenticationProvider(
requestMessage =>
{
// Append the access token to the request.
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("bearer", _token);
// Get event times in the current time zone.
requestMessage.Headers.Add("Prefer", "outlook.timezone=\"" + TimeZoneInfo.Local.Id + "\"");
return Task.CompletedTask;
}));
return graphClient;
}
Once graph client is created you can make a call to the intended graph api:
await client.CreateMeeting(meetingDetails).ConfigureAwait(false);
Please refer this sample code:
Graph Sample
I have IdentityServer4 and a web API in the same project, and I would like to use IdentityServer token handling in my own controllers, e.g. I want to issue tokens, refreshtokens, revoke tokens in my own controllers without calling other built in IdentityServer endpoints.
Are there services for stuff like this I'm missing?
Thanks
I need something like someIdentityServerService below
[AllowAnonymous]
[HttpPost("login")]
public async Task<IActionResult> Login([FromBody] UserLoginRequestDto model)
{
var user = await _userManager.FindByEmailAsync(model.Email);
if (user == null || !await _userManager.CheckPasswordAsync(user, model.Password))
{
return BadRequest("Wrong email or password!");
}
// do some other stuff
var token = await someIdentityServerService.GetAccessTokenAsync(user);
return Ok(token);
}
I do not think, it's a good idea to come inside the certified IdP and call the internals in your own order, not the order defined by the protocol.
Nevertheless you can do that by looking into the implementation and invoking or overriding. Identity Server is highly flexible, using standard DI you can override almost everything.
for instance
var identityPricipal = await _principalFactory.CreateAsync(user);
var identityUser = new IdentityServerUser(user.Id.ToString())
{
AdditionalClaims = identityPricipal.Claims.ToArray(),
DisplayName = user.UserName,
AuthenticationTime = DateTime.UtcNow,
IdentityProvider = IdentityServerConstants.LocalIdentityProvider
};
var request = new TokenCreationRequest();
request.Subject = identityUser.CreatePrincipal();
request.IncludeAllIdentityClaims = true;
request.ValidatedRequest = new ValidatedRequest();
request.ValidatedRequest.Subject = request.Subject;
request.ValidatedRequest.SetClient(Config.Clients().First());
request.Resources = new Resources(Config.IdentityResources(), new List<ApiResource>());
request.ValidatedRequest.Options = _options;
request.ValidatedRequest.ClientClaims = identityUser.AdditionalClaims;
var token = await _tokenService.CreateAccessTokenAsync(request);
token.Issuer = "...";
return await _tokenService.CreateSecurityTokenAsync(token);
More convenient way of creating tokens is to employ IdentityServerTools. However that's aim is to help calling external APIs from within IdSrv, not issuing any JWTs by external requests.
If you need to request JWTs from outside (and follow the protocol), you have to use predefined endpoints and signatures.
I'm encountering a problem. I am using Microsoft Graph to get the current logged in user via OnBehalfOfMsGraphAuthenticationProvider.cs as seen in the following solution.
This has been working flawlessly, but I have been doing some refactoring, and suddenly I get an error when trying to execute my authContext.AcquireTokenAsync() method.
HTTP Error 502.3 - Bad Gateway
The code in question looks like this:
public async Task AuthenticateRequestAsync(HttpRequestMessage request) {
var httpContext = _httpContextAccessor.HttpContext;
//Get the access token used to call this API
string token = await httpContext.GetTokenAsync("access_token");
//We are passing an *assertion* to Azure AD about the current user
//Here we specify that assertion's type, that is a JWT Bearer token
string assertionType = "urn:ietf:params:oauth:grant-type:jwt-bearer";
//User name is needed here only for ADAL, it is not passed to AAD
//ADAL uses it to find a token in the cache if available
var user = httpContext.User;
string userName =
user.FindFirst(ClaimTypes.Upn).Value ?? user.FindFirst(ClaimTypes.Email).Value;
var userAssertion = new UserAssertion(token, assertionType, userName);
//Construct the token cache
var cache = new DistributedTokenCache(user, _distributedCache,
_loggerFactory, _dataProtectionProvider);
var authContext = new AuthenticationContext(_configuration["AzureAd:Instance"] +
_configuration["AzureAd:TenantId"], true, cache);
var clientCredential = new ClientCredential(_configuration["AzureAd:ClientId"],
(string) _configuration["AzureAd:ClientSecret"]);
//Acquire access token
var result = await authContext.AcquireTokenAsync("https://graph.microsoft.com", clientCredential, userAssertion); //This is where it crashes
//Set the authentication header
request.Headers.Authorization = new AuthenticationHeaderValue(result.AccessTokenType, result.AccessToken);
}
I am calling it from my OrdersController:
// POST: api/Orders
[HttpPost]
public async Task<IActionResult> CreateAsync([FromBody] OrderDTO order) {
if (!ModelState.IsValid) {
return BadRequest(ModelState);
}
var graphUser = await this.graphApiService.GetUserProfileAsync();
The refactoring has consisted of dividing my solution into two class library projects and one web project - the latter has the controllers and the React app. GraphAPiClient and the provider are located in the Core library like this:
Screenshot of architecture
So, it turns out that the problem appeared when I upgraded the package Microsoft.IdentityModel.Clients.ActiveDirectory from v3.19.8 to v4.4.1. For some reason, no versions above v3.19.8 work with my code, causing it to crash when I try to make the call to https://graph.microsoft.com, but as soon as I downgraded the problem disappeared.
Try using AcquireToken instead of AcquireTokenAsync
azureAuthenticationContext.AcquireToken
EDIT 10/24
I think this was all likely user error - see my answer below for remedy before getting too deep into this question
TL;DR:
For my OAuth 2.0 code flow...
Why does my TokenCredentials not work with my AutoRest client?
I'm getting NO bearer token applied to the request / no Authorization header set
I know my pipeline works already..
Using code from this azure sample, which is NOT an AutoRest client, I can successfully get my access_token and can get JSON from my protected Web API project.. so I've ruled out all the prerequisite stuff.. I know my pipeline works
My AutoRest setup..
1.) Downloaded from GitHub this AutoRest repo v1.1.0
2.) Downloaded my swagger JSON to disk, saved as swagger.json
3.) Ran this command-line to generate C# files:
autorest --input-file=swagger.json --csharp --output-folder=MyCorp_ApiClient_Tsl --namespace='MyCorp.ApiClient' --add-credentials
4.) Copied generated classes into my .NET 4.6.2 web site
5.) These are my NuGets:
- Microsoft.Rest.ClientRuntime version="2.3.8"
- Microsoft.Rest.ClientRuntime.Azure.Authentication version="2.3.1"
- Microsoft.IdentityModel.Clients.ActiveDirectory version="2.28.3"
Here's what's not working:
AdalTokenHelper tokenHelper = new AdalTokenHelper();//helper code further below
string token = await tokenHelper.GetTokenString();
var svcClientCreds = new TokenCredentials(token, "Bearer");
client = new MyCorp.ApiClient(new Uri(apiRsrcUrl), svcClientCreds,
new DelegatingHandler[] { new MyAzureTracingHandler() });
//make call to OData controller...
MyCorp.ApiClient.Models.ODataResponseListStatus statusList = await client.Status.GetStatusAsync(expand: "StatusType",cancellationToken: defaultCancelThreadToken);
return View(statusList.Value);
I've tried variations of the above, using different ctor's of TokenCredentials, but no matter, I can put my breakpoint in MyAzureTracingHandler and see the request has no Authorization headers applied.. so I get the expected 401 Unauthorized response.
If I modify MyAzureTracingHandler to accept my instance of TokenCredentials then I can force the request to have the appropriate bearer token applied..
This works, but, feels hack-ish:
I changed my original client instantiation snippet from this:
client = new ApiClient(new Uri(apiRsrcUrl), svcClientCreds,
new DelegatingHandler[] { new MyAzureTracingHandler() });
To this:
client = new ApiClient(new Uri(apiRsrcUrl), svcClientCreds,
new DelegatingHandler[] { new MyAzureTracingHandler(svcClientCreds) });
And inside the SendAsync method of MyAzureTracingHander I do this:
await svcClientCreds.ProcessHttpRequestAsync(request, cancellationToken);
Am I doing something wrong? I don't think I should have to pass the ServiceClientCredentials in twice when instantiating my client.
Appendix A - Getting access token via ADAL:
private string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private string tslResourceID = ConfigurationManager.AppSettings["ross:TslWebApiResourceId"];
private static string loginRedirectUri = ConfigurationManager.AppSettings["ross:LoginRedirectUri"];
private AuthenticationContext authContext;
private AuthenticationResult authenticationResult;
public async Task<string> GetTokenString()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
string tenantID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/tenantid").Value;
string userObjectID = ClaimsPrincipal.Current.FindFirst("http://schemas.microsoft.com/identity/claims/objectidentifier").Value;
try
{
// get a token for the Graph without triggering any user interaction (from the cache, via multi-resource refresh token, etc)
ClientCredential clientcred = new ClientCredential(clientId, appKey);
// initialize AuthenticationContext with the token cache of the currently signed in user, as kept in the app's database
authContext = new AuthenticationContext(Startup.Authority, new ADALTokenCache(userObjectID));
UserIdentifier userIdentifier = new UserIdentifier(userObjectID, UserIdentifierType.UniqueId);
authenticationResult = await authContext.AcquireTokenSilentAsync(tslResourceID, clientcred, userIdentifier);
}
catch(AdalException ex)
{
throw ex;
}
return authenticationResult.AccessToken;
}
While I believe I ran my autorest command with --add-credentials it's possible I may have used the older syntax... --AddCredentials true
I also did not run autorest --reset as the docs recommend you do
One of these is the culprit, because now my 1.1.0 autorest installation is generating everything correctly.
I'm playing with OneDrive SDK 1.1.15.0:
try
{
AppConfig appConfig = new AppConfig
{
MicrosoftAccountAppId = oneDriveClientID, //something like 00000000123456AB
MicrosoftAccountClientSecret = oneDriveClientSecret, //something like 3vx[...]1sJ
MicrosoftAccountReturnUrl = "https://localhost/return",
MicrosoftAccountScopes = new string[] { "wl.signin", "wl.offline_access", "onedrive.readonly" }
};
OneDriveClient oneDriveClient = new OneDriveClient(appConfig);
AccountSession accountSession = await oneDriveClient.AuthenticateAsync();
//more code
await oneDriveClient.SignOutAsync();
}
catch (Exception ex)
{
throw ex;
}
My problem is in line:
AccountSession accountSession = await oneDriveClient.AuthenticateAsync();
that throws the following exception:
Microsoft.OneDrive.Sdk.OneDriveException, AuthenticationFailure: Failed to retrieve a valid authentication token for the user.
Any ideas?
Thank you in advance!
UPDATE
After reading comment from ginach (thank you!), I update my code. Some arguments to underline:
I want to access OneDrive from an Azure worker Role, so no authentication windows or something like that.
I upload the Microsoft.OneDrive SDK to 1.1.20 version.
I already registered my application to the OneDrive dev portal.
My actual code is:
try
{
MicrosoftAccountServiceInfo serviceInfo = new MicrosoftAccountServiceInfo();
serviceInfo.AppId = oneDriveClientID; //something like: 00000000ABCDEFGH
serviceInfo.ClientSecret = oneDriveClientSecret; //something like: 3vx[...]1sJ
serviceInfo.ReturnUrl = oneDriveReturnUrl; //something like: https://localhost/return
serviceInfo.Scopes = oneDriveAccountScopes; //something like new string[] { "wl.signin", "wl.offline_access", "onedrive.readonly" }
MicrosoftAccountAuthenticationProvider authenticationProvider = new MicrosoftAccountAuthenticationProvider(serviceInfo);
OneDriveClient oneDriveClient = await OneDriveClient.GetAuthenticatedMicrosoftAccountClient(oneDriveClientID, oneDriveReturnUrl, oneDriveAccountScopes, authenticationProvider);
//more code
await oneDriveClient.SignOutAsync();
}
catch (OneDriveException odex)
{
throw odex;
}
catch (Exception ex)
{
throw ex;
}
I obtain again and again (in OneDriveClient.GetAuthenticatedMicrosoftAccountClient method) a OneDriveException stating (Error property): AuthenticationFailure - Failed to retrieve a valid authentication token for the user.
Any suggestion?
Thank you.
UPDATE 2
OK, I'm trying a new approach. Using RestSharp I try to login to OneDrive with that code:
string clientId = "00[...]00";
string scopes = "wl.signin, wl.offline_access, onedrive.readonly";
string responseType = "code";
string redirectUri = "https://login.live.com/oauth20_desktop.srf";
RestClient client = new RestClient("https://login.live.com");
RestRequest request = new RestRequest();
request.Method = Method.GET;
request.Resource = "oauth20_authorize.srf";
request.AddQueryParameter("client_id", clientId);
request.AddQueryParameter("scope", scopes);
request.AddQueryParameter("response_type", responseType);
request.AddQueryParameter("redirect_uri", redirectUri);
IRestResponse response = client.Execute(request);
string content = response.Content;
I check the request with Fiddler and what I'm sending is:
https://login.live.com/oauth20_authorize.srf?client_id=00[...]00&scope=wl.signin%20wl.offline_access%20onedrive.readonly&response_type=code&redirect_uri=https%3A%2F%2Flogin.live.com%2Foauth20_desktop.srf
But OneDrive server answers my with:
Microsoft account requires JavaScript to sign in. This web browser either does not support JavaScript, or scripts are being blocked. To find out whether your browser supports JavaScript, or to allow scripts, see the browser's online help.
So I try the request in a browser and OneDrive server redirects me to the authorization page:
Now the question is: is there any workaround to skip the manual authorization?
Thank you,
Attilio
The client requires an authentication provider to be able to retrieve authentication tokens. There are a few ways to do this depending on your current platform.
Create your own IAuthenticationProvider implementation. The authentication provider is responsible for setting the Authentication header on requests. Here's how you would create a client instance with a custom authentication provider:
var client = new OneDriveClient(appConfig, serviceInfoProvider: new
ServiceInfoProvider(new CustomAuthenticationProvider()));
Use one of the various default authentication implementations. Take a look at the SDK authentication documentation for the available options and examples.
If you have a refresh token and only want to do the silent authentication flow you can use OneDriveClient.GetSilentlyAuthenticatedMicrosoftAccountClient. Here's an example:
var client = await OneDriveClient.GetSilentlyAuthenticatedMicrosoftAccountClient(clientId, returnUrl, scopes, refreshToken);