This article is part of a series called Setting up an Authorization Server with OpenIddict. The articles in this series will guide you through the process of setting up an OAuth2 + OpenID Connect authorization server on the the ASPNET Core platform using OpenIddict.
- Part I: Introduction
- Part II: Create ASPNET project
- Part III: Client Credentials Flow
- Part IV: Authorization Code Flow
- Part V: OpenID Connect
- Part VI: Refresh tokens
robinvanderknaap / authorization-server-openiddict
Authorization Server implemented with OpenIddict.
In this part we will implement the Authorization Code Flow with PKCE extension. This flow is the recommended flow for Single Page Applications (SPA's) and native/mobile applications.
Configure OpenIddict
First we need to enable the Authorization Code Flow in Startup.cs
:
public void ConfigureServices(IServiceCollection services)
{
...
services.AddOpenIddict()
...
.AddServer(options =>
{
options.AllowAuthorizationCodeFlow().RequireProofKeyForCodeExchange();
...
options
.SetAuthorizationEndpointUris("/connect/authorize")
.SetTokenEndpointUris("/connect/token");
...
options
.UseAspNetCore()
.EnableTokenEndpointPassthrough()
.EnableAuthorizationEndpointPassthrough();
});
...
}
The call AllowAuthorizationCodeFlow
enables the flow, RequireProofKeyForCodeExchange
is called directly after that, this makes sure all clients are required to use PKCE (Proof Key for Code Exchange).
The authorization code flow dictates that the user first authorizes the client to make requests in the user's behalf. Therefore, we need to implement an authorization endpoint which returns an authorization code to the client when the user allows it.
The client can exchange the authorization code for an access token by calling the token endpoint we already created for the client credentials flow.
First, we will create the authorization endpoint, the call to EnableAuthorizationEndpointPassthrough
in Startup.cs
allows us to implement the endpoint within a controller.
After that we will make some minor adjustments to our token endpoint to allow clients to exchange authorization codes for access tokens.
Authorization endpoint
We'll implement the authorization endpoint in the Authorization Controller, just like the token endpoint:
[HttpGet("~/connect/authorize")]
[HttpPost("~/connect/authorize")]
[IgnoreAntiforgeryToken]
public async Task<IActionResult> Authorize()
{
var request = HttpContext.GetOpenIddictServerRequest() ??
throw new InvalidOperationException("The OpenID Connect request cannot be retrieved.");
// Retrieve the user principal stored in the authentication cookie.
var result = await HttpContext.AuthenticateAsync(CookieAuthenticationDefaults.AuthenticationScheme);
// If the user principal can't be extracted, redirect the user to the login page.
if (!result.Succeeded)
{
return Challenge(
authenticationSchemes: CookieAuthenticationDefaults.AuthenticationScheme,
properties: new AuthenticationProperties
{
RedirectUri = Request.PathBase + Request.Path + QueryString.Create(
Request.HasFormContentType ? Request.Form.ToList() : Request.Query.ToList())
});
}
// Create a new claims principal
var claims = new List<Claim>
{
// 'subject' claim which is required
new Claim(OpenIddictConstants.Claims.Subject, result.Principal.Identity.Name),
new Claim("some claim", "some value").SetDestinations(OpenIddictConstants.Destinations.AccessToken)
};
var claimsIdentity = new ClaimsIdentity(claims, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
// Set requested scopes (this is not done automatically)
claimsPrincipal.SetScopes(request.GetScopes());
// Signing in with the OpenIddict authentiction scheme trigger OpenIddict to issue a code (which can be exchanged for an access token)
return SignIn(claimsPrincipal, OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
}
Unlike the Clients Credentials Flow, the Authorization Code Flow involves the end user for approval. Remember that we already implemented authentication in our project. So, in the authorize method we just determine if the user is already logged in, if not, we redirect the user to the login page.
When the user is authenticated, a new claims principal is created which is used to sign in with OpenIddict authentication scheme.
You can add claims to principal which will be added to the access token if the destination is set to AccessToken
. The subject
claim is required and is always added to the access token, you don't have to specify a destination for this claim.
The scopes requested by the client are all given with the call claimsPrincipal.SetScopes(request.GetScopes())
, because we don't implement a consent screen in this example to keep things simple. When you do implement consent, this would be the place to filter the requested scopes.
The SignIn
call triggers the OpenIddict middleware to send an authorization code which the client can exchange for an access token by calling the token endpoint.
We need to alter the token endpoint also since we now support the Authorization Code Flow:
[HttpPost("~/connect/token"), Produces("application/json")]
public async Task<IActionResult> Exchange()
{
...
if (request.IsClientCredentialsGrantType())
{
...
}
else if (request.IsAuthorizationCodeGrantType())
{
// Retrieve the claims principal stored in the authorization code
claimsPrincipal = (await HttpContext.AuthenticateAsync(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme)).Principal;
}
else
{
throw new InvalidOperationException("The specified grant type is not supported.");
}
...
}
The claims principal we created in the authorize method is stored in the authorization code, so we only need to grab the claims principal from the request and pass it to the SignIn
method. OpenIddict will respond with an access token.
Now, let's see if we can request an access token with the Authorization Code Flow using Postman:
This won't work because the client is not allowed to use the Authorization Code Flow and also we did not specify the RedirectUris
of the client.
The redirect URI in the case of Postman is https://oauth.pstmn.io/v1/callback
. The authorization code is sent here after successful authentication.
After updating, the Postman client in TestData.cs
should look like this:
ClientId = "postman",
ClientSecret = "postman-secret",
DisplayName = "Postman",
RedirectUris = { new Uri("https://oauth.pstmn.io/v1/callback") },
Permissions =
{
OpenIddictConstants.Permissions.Endpoints.Authorization,
OpenIddictConstants.Permissions.Endpoints.Token,
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
OpenIddictConstants.Permissions.GrantTypes.ClientCredentials,
OpenIddictConstants.Permissions.Prefixes.Scope + "api",
OpenIddictConstants.Permissions.ResponseTypes.Code
}
If everything is working correctly, you should be able to obtain an access token with Postman after authenticating yourself.
Note. A client secret is optional when configuring a client with OpenIddict, this is useful for public clients which aren't able to securely store a client secret. When the client secret is omitted from the configuration, you can also omit it from the request.
The PKCE-enhanced Authorization Code Flow introduces a secret created by the calling application that can be verified by the authorization server (source). However, PKCE doesn't replace client secrets. PKCE and client secrets are complementary and you should use them both when possible (typically, for server-side apps). (Explained to me by the author of OpenIddict)
Next
Congratulations, you have implemented the Authentication Code Flow with PKCE with OpenIddict! Next up, we will demonstrate how to leverage the OpenID Connect protocol.
Top comments (16)
Hello guys!
I followed this guide but I noticed that the PKCE flow needs a client_secret to be accomplished. I was thinking the PKCE flow is just done to avoid exchange of client secret from a SPA to the Auth Server. Am I right? What can I do to avoid openiddict to ask for a client_secret?
First of all, Thanks to Robin for this amazing tutorial.
@skini82 , had you got any private answer to this issue?? I'm getting the same problem and I don't know how to configure Openiddict to avoid the client_secret validation in a "code flow + pkce" setting...
When my SPA client request the token(post to the token endpoint) with this parameters:
grant_type=authorization_code
&code=mgJkm0ivM******************CV6m6ZBGEKMLc598
&redirect_uri=redirect_uri
&code_verifier=MFVtUFZyRGVq**************VteFRpTncwUzB0OWlSRGM1
&client_id=security.***.dev
Openiddict , is validating the client_secret and respond with a :
OpenIddict.Server.OpenIddictServerDispatcher: Information: The token request was rejected because the confidential application 'security.*****.dev' didn't specify a client secret.
OpenIddict.Server.OpenIddictServerDispatcher: Information: The response was successfully returned as a JSON document: {
"error": "invalid_client",
"error_description": "The 'client_secret' parameter required for this client application is missing.",
"error_uri": "documentation.openiddict.com/error..."
}.
I'm a little confuse about this , for the same reason that you were
Any help is appreciated.
Thanks!
Ok...well....after days thinking about posting my question or not, a few minutes after I did it...I have found the solution: I realised than my App_client was configured as "confidential" (what I suppouse is intended for server-side apps or very confident environments). For a public spa the attribute *"Type" should be "public" *, in this way, Openiddict doesn't validate the client_secret...good to know
man, can't describe how much it helped me. i searched high and low before ran into your comment.
await manager.CreateAsync(new OpenIddictApplicationDescriptor
{
ClientId = MyConstants.LibraryAngularApp,
Type = "public", // !!!
}
I'm glad to hear that!
OpenId has a constant for this :
Type = OpenIddictConstants.ClientTypes.Public,
Been wrapping my head around authentication code flow for years. This example clarifies many things for me.
When i call the userinfo i got this :
The userinfo request was rejected because the mandatory 'access_token' parameter was missing.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The response was successfully returned as a challenge response: {
"error": "missing_token",
"error_description": "The mandatory 'access_token' parameter is missing.",
"error_uri": "documentation.openiddict.com/error..."
}.
I got same error "The mandatory 'access_token' parameter is missing.", but I understand where the problem is.
It's not enough to open
/connect/userinfo
address in browser. You should attach previously created token, so for/connect/userinfo
request you should send a 'GET' request with attached token via Postman!any fix ?
Very good solution now that identityserver is paid.
Question. As handling of various scopes.
I'm still going through the series and picking out how things will apply to my own setup, but I wanted to pause and thank you for a very well put together series on authentication. This may be the single best walkthrough I've found for configuring API authentication in core.
Registered just to say thank you for such an amazing article!
Thank you, very nice to hear that!
Very good article!