I have an ASP.NET Core MVC application allowing anonymous users. This app is calling an ASP.NET Web API that is protected by Identity Server 4. I have created a client in Identity Server describing the MVC app (client) and given it access to the api scope like this:
new Client
{
ClientId = "my-mvc-client-app",
AllowedGrantTypes = GrantTypes.ClientCredentials,
RequireConsent = false,
ClientSecrets = new List<Secret> { new Secret("this-is-my-secret".Sha256()) },
AllowedScopes = new List<string>
{
StandardScopes.OpenId.Name,
StandardScopes.Profile.Name,
StandardScopes.OfflineAccess.Name,
"my-protected-api"
},
RedirectUris = new List<string>
{
"http://localhost:5009/signin-oidc",
}
}
In my MVC app, I'm using TokenClient to get a token that I can use when making requests to the protected API like this:
var disco = await DiscoveryClient.GetAsync("http://localhost:5010");
var tokenClient = new TokenClient(disco.TokenEndpoint, clientId, clientSecret);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync("hrmts-test-candidate-api-scope");
This works fine, but I'm requesting new tokens from Identity Server on every request, which is probably not a good idea.
What is the best practice for handling the tokens? How can I persist them on the client (the MVC app) and how can I handle refresh tokens to make sure the client gets a new token when necessary?
You need to wrap that client in a managed service of some kind (as a singleton) so that you can use it anywhere you need. We have a token component that we use for server to server communication that follows this flow:
public class ServerTokenComponent
{
private TokenResponse Token { get; set; }
private DateTime ExpiryTime { get; set; }
public async Task<TokenResponse> GetToken()
{
//use token if it exists and is still fresh
if (Token != null && ExpiryTime > DateTime.UtcNow)
{
return Token;
}
//else get a new token
var client = new TokenClient("myidpauthority.com","theclientId","thesecret")
var scopes = "for bar baz";
var tokenResponse = await client.RequestClientCredentialsAsync(scopes);
if (tokenResponse.IsError || tokenResponse.IsHttpError)
{
throw new SecurityTokenException("Could not retrieve token.");
}
//set Token to the new token and set the expiry time to the new expiry time
Token = tokenResponse;
ExpiryTime = DateTime.UtcNow.AddSeconds(Token.ExpiresIn);
//return fresh token
return Token;
}
}
In other words - you need to cache that token somehow. When you request the token, you get an ExpiresIn in the response - this will tell you how long the token will be valid.
Another option is to wait until the API returns a 401 - and then request a new token.
Refresh tokens are not used with client credentials flow.
Related
I have a WebApi2 app which servers as api for my app frontend. Now i want to use AD B2C to manage my users - let's say I want to differentiate them by their roles (admin or customer) and for that i created two b2c users groups accordingly. When user logs in i want to display different things for users with different roles (groups).
I'm using this example to setup Startup.Auth.cs in my WebApi2 project:
var tvps = new TokenValidationParameters
{
ValidAudience = clientId,
AuthenticationType = signUpSignInPolicy,
};
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions
{
AccessTokenFormat = new JwtFormat(tvps, new OpenIdConnectCachingSecurityTokenProvider(String.Format(aadInstance, tenant, defaultPolicy))),
});
From what I have read b2c doesn't return user's grups in claims for now. Some people suggested I need to call GraphApi after obtaining token to fetch these groups and add them to user's claims:
private static async Task<string> GetGroups(string token, string userId)
{
using (var client = new HttpClient())
{
var requestUrl = $"https://graph.microsoft.com/v1.0/users/{userId}/memberOf?$select=displayName";
var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
var response = await client.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
return responseString;
}
}
This is where I've stuck. How can I inject my code to get token for calling graph? I've messed with OAuthBearerAuthenticationOptions.Provider:
Provider = new OAuthBearerAuthenticationProvider
{
OnValidateIdentity = (context) =>
{
// var token = ??
// var userId = <get from context's claims>
// var groups = GetGroups(token, userId);
// <add to claims>
return Task.CompletedTask;
}
},
...but I don't know how to get to token. Maybe that's wrong from the start and I need another approach?
Customer's token cannot be used to call AADGraph/MSGraph Apis. To get token to call graph apis in an automated way, we need app-only access. We need to configre an app in the tenant, the crendetial of which are used to get a token. That token can then be used to call memberOF Api (or any other api which does or require user information to be there)
Here is the sample and explaination of how to call AAD Graph apis in a B2C dependent service.
https://learn.microsoft.com/en-us/azure/active-directory-b2c/active-directory-b2c-devquickstarts-graph-dotnet
Hello I have developed a Microsoft application using Microsoft Graph API in order to obtain planner data and store it in a database for now. On it's own the application works fine without any issue what so ever.
The next task for me is to integrate this separate application into the main company application. The main company's website uses form authentication. What is the best way to integrate this. Currently when I try to login to get authorized I am redirected to the form login not the Microsoft one
I have registered the application in the Microsoft application registration pool. I have also added the office 365 api
This is the token obtain code that i am using
public async Task<string> GetUserAccessTokenAsync()
{
string signedInUserID = ClaimsPrincipal.Current.FindFirst(ClaimTypes.NameIdentifier).Value;
tokenCache = new SessionTokenCache(
signedInUserID,
HttpContext.Current.GetOwinContext().Environment["System.Web.HttpContextBase"] as HttpContextBase);
//var cachedItems = tokenCache.ReadItems(appId); // see what's in the cache
ConfidentialClientApplication cca = new ConfidentialClientApplication(
appId,
redirectUri,
new ClientCredential(appSecret),
tokenCache);
try
{
AuthenticationResult result = await cca.AcquireTokenSilentAsync(scopes.Split(new char[] { ' ' }));
return result.Token;
}
// Unable to retrieve the access token silently.
catch (MsalSilentTokenAcquisitionException)
{
HttpContext.Current.Request.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties() { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
throw new Exception(Resource.Error_AuthChallengeNeeded);
}
}
This is the sign in method I am trying use when trying to directly log in
// Signal OWIN to send an authorization request to Azure.
HttpContext.GetOwinContext().Authentication.Challenge(
new AuthenticationProperties { RedirectUri = "/" },
OpenIdConnectAuthenticationDefaults.AuthenticationType);
I have solved this issue by implementing the following code
public ActionResult SignIn()
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
string redirectUri = Url.Action("Authorize", "Planner", null, Request.Url.Scheme);
Uri authUri = authContext.GetAuthorizationRequestURL("https://graph.microsoft.com/", SettingsHelper.ClientId,
new Uri(redirectUri), UserIdentifier.AnyUser, null);
// Redirect the browser to the Azure signin page
return Redirect(authUri.ToString());
}
public async Task<ActionResult> Authorize()
{
// Get the 'code' parameter from the Azure redirect
string authCode = Request.Params["code"];
AuthenticationContext authContext = new AuthenticationContext(SettingsHelper.AzureADAuthority);
// The same url we specified in the auth code request
string redirectUri = Url.Action("Authorize", "Planner", null, Request.Url.Scheme);
// Use client ID and secret to establish app identity
ClientCredential credential = new ClientCredential(SettingsHelper.ClientId, SettingsHelper.ClientSecret);
try
{
// Get the token
var authResult = await authContext.AcquireTokenByAuthorizationCodeAsync(
authCode, new Uri(redirectUri), credential, SettingsHelper.O365UnifiedResource);
// Save the token in the session
Session["access_token"] = authResult.AccessToken;
return Redirect(Url.Action("Index", "Planner", null, Request.Url.Scheme));
}
catch (AdalException ex)
{
return Content(string.Format("ERROR retrieving token: {0}", ex.Message));
}
}
A link to the solution that helped tackle this was this. It's slightly old but still helped out massively
https://www.vrdmn.com/2015/05/using-office-365-unified-api-in-aspnet.html
My goal is to protect a Web API, such that it can only be accessed by a client using an access token issued by IS based on Windows authentication. I worked through this basic sample:
http://docs.identityserver.io/en/release/quickstarts/1_client_credentials.html
Now, I need to extend the basic sample such that the access token returned to the client is issued based on Windows authentication. More specifically, I need to have the user (which is executing the client application) to be authenticated against Active Directory when requesting an access token. How should this be done?
I have already been running the quick start (https://github.com/IdentityServer/IdentityServer4.Templates) successfully, where the login is based on a Windows external provider, but I cannot figure out how to adopt this functionality to my strategy.
I tried using an Extension Grant (http://docs.identityserver.io/en/release/topics/extension_grants.html) and have the ValidateAsync() method be the one to do the authentication against AD, but could not make it work (primarily since HttpContext is not available). Is this even the correct approach?
Update
In this system, the client is a console application (without human interaction), thus the context is the account running the application.
I have been running the QuickstartUI and see how the AccountController logic handles the "Windows" button, but cannot grasp how to combine this with requesting access tokens. My client code goes like this:
static async Task Main(string[] args)
{
var disco = await DiscoveryClient.GetAsync("http://localhost:50010");
var tokenClient = new TokenClient(disco.TokenEndpoint);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("CustomWindows"); // Not sure about this
var client = new HttpClient();
client.SetBearerToken(tokenResponse.AccessToken);
var response = await client.GetAsync("http://localhost:50011/api/identity");
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
Console.ReadLine();
}
I am not sure how to use the TokenClient to get an access token in this case. I would prefer not to store and use passwords, but have IS issue access tokens based on authenciating the client context against AD. If implicit or hybrid flows must be used in this case, how must that be done?
I had the same requirement and implemented it using an extension grant.
This is the code of the extension grant:
public class WinAuthGrantValidator : IExtensionGrantValidator
{
private readonly HttpContext httpContext;
public string GrantType => WinAuthConstants.GrantType;
public WinAuthGrantValidator(IHttpContextAccessor httpContextAccessor)
{
httpContext = httpContextAccessor.HttpContext;
}
public async Task ValidateAsync(ExtensionGrantValidationContext context)
{
// see if windows auth has already been requested and succeeded
var result = await httpContext.AuthenticateAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
if (result?.Principal is WindowsPrincipal wp)
{
context.Result = new GrantValidationResult(wp.Identity.Name, GrantType, wp.Claims);
}
else
{
// trigger windows auth
await httpContext.ChallengeAsync(WinAuthConstants.WindowsAuthenticationSchemeName);
context.Result = new GrantValidationResult { IsError = false, Error = null, Subject = null };
}
}
}
And this is the client code:
var httpHandler = new HttpClientHandler
{
UseDefaultCredentials = true,
};
// request token
var tokenClient = new TokenClient(disco.TokenEndpoint, "client", "secret", httpHandler, AuthenticationStyle.PostValues);
var tokenResponse = await tokenClient.RequestCustomGrantAsync("windows_auth", "api1");
I'm using the refresh flow sample, but i added the instrospection middleware since my resource server is separated from my authorization server. Using the angular app, I'm able to connect to the authorization server. Everything works. But I noticed in the Implicit Flow sample that the following code was added:
if (await manager.FindByClientIdAsync("aurelia", cancellationToken) == null)
{
var descriptor = new OpenIddictApplicationDescriptor
{
ClientId = "aurelia",
DisplayName = "Aurelia client application",
PostLogoutRedirectUris = { new Uri("http://localhost:9000/signout-oidc") },
RedirectUris = { new Uri("http://localhost:9000/signin-oidc") }
};
await manager.CreateAsync(descriptor, cancellationToken);
}
Do I need to add something similar to this for the Refresh flow?
If you don't configure OpenIddict to require a client_id for token requests (by calling options.RequireClientIdentification()) then creating an application in the database is not mandatory to be able to use the refresh token flow.
I am using Google APIs for OAuth2 version 1.9 and trying to send AccessType as offline and ApprovalPrompt as force everytime so that I get a refresh token. I know there are many questions on this topic here in various api versions and languages. However, none of the solutions works with the new google library.
I am using the following to get the flow:
private IAuthorizationCodeFlow GetAuthorizationCodeFlow()
{
var flow = new GoogleAuthorizationCodeFlow(
new GoogleAuthorizationCodeFlow.Initializer
{
ClientSecrets = new ClientSecrets
{
ClientId =
"***",
ClientSecret = "***"
},
Scopes = new string[]
{
CalendarService.Scope.Calendar,
PlusService.Scope.UserinfoProfile,
PlusService.Scope.UserinfoEmail,
PlusService.Scope.PlusLogin,
"https://www.googleapis.com/auth/contacts.readonly"
}
});
return flow;
}
and then using the following code to get the token:
var token = flow.ExchangeCodeForTokenAsync("me", code,
uri.Substring(0, uri.IndexOf("?")), CancellationToken.None).Result;
This is where I need the refresh token every time (not just the first time) so I want to set the AccessType and ApprovalPrompt.
I also had the exact same question. Credit goes to this post.
I will paste my code as I had to make a small change to get it working.
(to use GoogleAuthorizationCodeFlow.Initializer in the constructor rather than AuthorizationCodeFlow.Initializer)
Implement your own GoogleAuthorizationCodeFlow class and set the "offline" access in the AccessType property. This will give you the refreshtoken.
public class OfflineAccessGoogleAuthorizationCodeFlow : GoogleAuthorizationCodeFlow
{
public OfflineAccessGoogleAuthorizationCodeFlow(GoogleAuthorizationCodeFlow.Initializer initializer) : base(initializer) { }
public override AuthorizationCodeRequestUrl CreateAuthorizationCodeRequest(string redirectUri)
{
return new GoogleAuthorizationCodeRequestUrl(new Uri(AuthorizationServerUrl))
{
ClientId = ClientSecrets.ClientId,
Scope = string.Join(" ", Scopes),
RedirectUri = redirectUri,
AccessType = "offline",
ApprovalPrompt = "force"
};
}
};
It's unfortunate that we can't set the "access_type" from GoogleAuthorizationCodeFlow Initializer.
Also, I use following code to retrieve the refresh token,
Google.Apis.Auth.OAuth2.Responses.TokenResponse token = new Google.Apis.Auth.OAuth2.Responses.TokenResponse();
//// Checks if the token is out of date and refresh the access token using the refresh token.
if (result.Credential.Token.IsExpired(SystemClock.Default))
{
//If the token is expired recreate the token
token = await result.Credential.Flow.RefreshTokenAsync(userid.ToString(), result.Credential.Token.RefreshToken, CancellationToken.None);
//Get the authorization details Results
result = await new AuthorizationCodeMvcApp(this, new AppFlowMetadata()).AuthorizeAsync(cancellationToken);
}