DEV Community

Cover image for Setting up an Authorization Server with OpenIddict - Part IV - Authorization Code Flow
Robin van der Knaap
Robin van der Knaap

Posted on • Updated on

Setting up an Authorization Server with OpenIddict - Part IV - Authorization Code Flow

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.

GitHub logo 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(); 
        });

        ...
}
Enter fullscreen mode Exit fullscreen mode

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);
}
Enter fullscreen mode Exit fullscreen mode

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.");
    }

    ...
}
Enter fullscreen mode Exit fullscreen mode

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:

Alt Text

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
}
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
skini82 profile image
Dario Fraschini

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?

Collapse
 
salvagl profile image
salvagl

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!

Collapse
 
salvagl profile image
salvagl

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

Thread Thread
 
khomenmac profile image
Khomenko Max

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", // !!!
}

Thread Thread
 
salvagl profile image
salvagl

I'm glad to hear that!

Thread Thread
 
rezapouya profile image
Reza Pouya

OpenId has a constant for this :

Type = OpenIddictConstants.ClientTypes.Public,

Collapse
 
bluearth profile image
Barkah Yusuf

Been wrapping my head around authentication code flow for years. This example clarifies many things for me.

Collapse
 
borisgr04 profile image
borisgr04

Very good solution now that identityserver is paid.
Question. As handling of various scopes.

Collapse
 
zizounetgit profile image
zizounetgit

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..."
}.

Collapse
 
mikhailcrimea profile image
Mikhail • Edited

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!

Collapse
 
tecno14 profile image
Wael Had

any fix ?

Collapse
 
nonsenseless profile image
nonsenseless

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.

Collapse
 
emman122 profile image
emman122 • Edited

If I generate the token using Insomia both the token and the Refresh token work, but if the refresh is requested again, it generates this error only with header basic

}.
info: OpenIddict.Server.OpenIddictServerDispatcher[0]
The token request was rejected because the mandatory 'client_id' parameter was missing.
fail: Microsoft.AspNetCore.Server.Kestrel[13]
Connection id "0HN0SUU7272TQ", Request id "0HN0SUU7272TQ:00000005": An unhandled exception was thrown by the application.
System.InvalidOperationException: Invalid non-ASCII or control character in header: 0x00E1

Image description

Image description

But if I use Postman the refresh touch is generated without any problems

Image description

Collapse
 
hypervtechnics profile image
hypervtechnics

Very good article!

Collapse
 
dubik profile image
dubik

Registered just to say thank you for such an amazing article!

Collapse
 
robinvanderknaap profile image
Robin van der Knaap

Thank you, very nice to hear that!