Implement an OAuth 2.0 Server (Part 10)

Welcome to the tenth part of a series of posts where we will implement an OAuth 2 Server using AspNet.Security.OpenIdConnectServer.

Authorization Provider - Token Methods

We’re still implementing the Providers/OAuthProvider.cs class we made in the previous section. Here we’re going to deal with the three Token methods we left un-overridden from last time: ValidateTokenRequest, HandleTokenRequest, ApplyTokenResponse.

Validate Token Request

As a small warning, the validate token request endpoint is one of the longest methods we’ll be implementing. To make it easier to digest, each significant sub-part has been split into its own mini section.

You’ll notice that each of the request types is handled like the authorization validation was - check the existence of our client id, that the supplied redirects, if necessary, match properly, and if a client secret is supplied, we check that its the correct secret for the client requesting permission.

Check for supported grant types

First we’ll automatically reject any request that isn’t one of our supported flows:

 1public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
 2
 3    VService = context.HttpContext.RequestServices.GetRequiredService<ValidationService>();
 4
 5    // We only accept "authorization_code", "refresh", "token" for this endpoint.
 6    if (!context.Request.IsAuthorizationCodeGrantType() 
 7        && !context.Request.IsRefreshTokenGrantType()
 8        && !context.Request.IsClientCredentialsGrantType()) {
 9        context.Reject(
10                error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
11                description: "Only authorization code, refresh token, and token grant types are accepted by this authorization server."
12            );
13    }
14
15
16    ...
17
18}

Recall that implicit grants are handled by the authorization methods from the previous section - meaning we don’t have to deal with any unnecessary bits here.

Set up the variables we’ll be using

Each of the grants we’ll deal with shares some subset of these variables, so we may as well declare them upfront, instead of repeatedly declaring them later:

 1public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
 2
 3    ...
 4
 5    string clientid = null;
 6    string clientsecret = null;
 7    string redirecturi = null;
 8    string code = null;
 9    string refreshtoken = null;
10    
11    ...
12
13}

Tackle the Authorization Code grant

Now we’ll handle the authorization_code grant. We won’t get around to testing this part for a while, but it’s the second step of the authorization code flow:

 1public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
 2
 3    ...
 4
 5    // Validating the Authorization Code Token Request
 6    if (context.Request.IsAuthorizationCodeGrantType()) {
 7        clientid = context.ClientId;
 8        clientsecret = context.ClientSecret;
 9        code = context.Request.Code;
10        redirecturi = context.Request.RedirectUri;
11
12        if (String.IsNullOrWhiteSpace(clientid)) {
13            context.Reject(
14                        error: OpenIdConnectConstants.Errors.InvalidClient,
15                        description: "client_id cannot be empty"
16                    );
17            return;
18        }
19        else if (String.IsNullOrWhiteSpace(clientsecret)) {
20            context.Reject(
21                        error: OpenIdConnectConstants.Errors.InvalidClient,
22                        description: "client_secret cannot be empty"
23                    );
24            return;
25        }
26        else if (String.IsNullOrWhiteSpace(redirecturi)) {
27            context.Reject(
28                        error: OpenIdConnectConstants.Errors.InvalidClient,
29                        description: "redirect_uri cannot be empty"
30                    );
31            return;
32        }
33        else if (!await VService.CheckClientIdIsValid(clientid)) {
34            context.Reject(
35                        error: OpenIdConnectConstants.Errors.InvalidClient,
36                        description: "The supplied client id was does not exist"
37                    );
38            return;
39        }
40        else if (!await VService.CheckClientIdAndSecretIsValid(clientid, clientsecret)) {
41            context.Reject(
42                        error: OpenIdConnectConstants.Errors.InvalidClient,
43                        description: "The supplied client secret is invalid"
44                    );
45            return;
46        }
47        else if (!await VService.CheckRedirectURIMatchesClientId(clientid, redirecturi)) {
48            context.Reject(
49                        error: OpenIdConnectConstants.Errors.InvalidClient,
50                        description: "The supplied redirect uri is incorrect"
51                    );
52            return;
53        }
54
55        context.Validate();
56        return;
57    }
58
59    ...
60}

Tackle the Refresh Token grant

Next we’ll handle the refresh_token, which is the third step and last of the authorization code flow steps

 1public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
 2
 3    ...
 4
 5    // Validating the Refresh Code Token Request
 6    else if (context.Request.IsRefreshTokenGrantType()) {
 7        clientid = context.Request.ClientId;
 8        clientsecret = context.Request.ClientSecret;
 9        refreshtoken = context.Request.RefreshToken;
10
11        if (String.IsNullOrWhiteSpace(clientid)) {
12            context.Reject(
13                        error: OpenIdConnectConstants.Errors.InvalidClient,
14                        description: "client_id cannot be empty"
15                    );
16            return;
17        }
18        else if (String.IsNullOrWhiteSpace(clientsecret)) {
19            context.Reject(
20                        error: OpenIdConnectConstants.Errors.InvalidClient,
21                        description: "client_secret cannot be empty"
22                    );
23            return;
24        }
25        else if (!await VService.CheckClientIdIsValid(clientid)) {
26            context.Reject(
27                        error: OpenIdConnectConstants.Errors.InvalidClient,
28                        description: "The supplied client id does not exist"
29                    );
30            return;
31        }
32        else if (!await VService.CheckClientIdAndSecretIsValid(clientid, clientsecret)) {
33            context.Reject(
34                        error: OpenIdConnectConstants.Errors.InvalidClient,
35                        description: "The supplied client secret is invalid"
36                    );
37            return;
38        }
39        else if (!await VService.CheckRefreshTokenIsValid(refreshtoken)) {
40            context.Reject(
41                        error: OpenIdConnectConstants.Errors.InvalidClient,
42                        description: "The supplied refresh token is invalid"
43                    );
44            return;
45        }
46
47        context.Validate();
48        return;
49    }
50
51    ...
52
53}

Tackle Client Credentials grant

 1public override async Task ValidateTokenRequest(ValidateTokenRequestContext context) {
 2
 3    ...
 4
 5    // Validating Client Credentials Request, aka, 'token'
 6    else if (context.Request.IsClientCredentialsGrantType()) {
 7        string clientid = context.ClientId;
 8        string clientsecret = context.ClientSecret;
 9
10
11        if (String.IsNullOrWhiteSpace(clientid)) {
12            context.Reject(
13                        error: OpenIdConnectConstants.Errors.InvalidClient,
14                        description: "client_id cannot be empty"
15                    );
16            return;
17        }
18        else if (String.IsNullOrWhiteSpace(clientsecret)) {
19            context.Reject(
20                        error: OpenIdConnectConstants.Errors.InvalidClient,
21                        description: "client_secret cannot be empty"
22                    );
23            return;
24        }
25        else if (!await VService.CheckClientIdIsValid(clientid)) {
26            context.Reject(
27                        error: OpenIdConnectConstants.Errors.InvalidClient,
28                        description: "The supplied client id does not exist"
29                    );
30            return;
31        }
32        else if (!await VService.CheckClientIdAndSecretIsValid(clientid, clientsecret)) {
33            context.Reject(
34                        error: OpenIdConnectConstants.Errors.InvalidClient,
35                        description: "The supplied client secret is invalid"
36                    );
37            return;
38        }
39
40        context.Validate();
41        return;
42    }
43
44    ...
45
46}

Error Catchall

Just in case anything managed to make it this far without being validated, we’ll just blanket reject the request.

 1public class OAuthProvider : OpenIdConnectServerProvider {
 2
 3    ...
 4
 5    else {
 6        context.Reject(
 7            error: OpenIdConnectConstants.Errors.ServerError,
 8            description: "Could not validate the token request"
 9        );
10        return;
11    }
12}

Handle Token Request

We said we’d outsource the Handle for the Authorization requests to another method, but we do have to actually deal with it here in the Token flow. This method is itself not so complicated because we still end up outsourcing some major parts to another section of the application, namely, the TicketCounter - which we’ll get to later.

For now it’s just important to understand that unless ASOS has a valid, non-null AuthenticationTicket, it cannot issue any tokens properly and will fail if you attempt it. Authentication tickets are a complicated subject that will be explained when we get around to implementing the TicketCounter - until then, just hold on.

 1public override Task HandleTokenRequest(HandleTokenRequestContext context) {
 2    AuthenticationTicket ticket = null;
 3    // Handling Client Credentials
 4    if (context.Request.IsClientCredentialsGrantType()) {
 5        // If we do not specify any form of Ticket, or ClaimsIdentity, or ClaimsPrincipal, our validation will succeed here but fail later.
 6        // ASOS needs those to serialize a token, and without any, it fails because there's way to fashion a token properly. Check the ASOS source for more details.
 7        ticket = TicketCounter.MakeClaimsForClientCredentials(context.Request.ClientId);
 8        context.Validate(ticket);
 9        return Task.CompletedTask;
10    }
11    // Handling Authorization Codes
12    else if (context.Request.IsAuthorizationCodeGrantType() || context.Request.IsRefreshTokenGrantType()) {
13        ticket = context.Ticket;
14        if (ticket != null) {
15            context.Validate(ticket);
16            return Task.CompletedTask;
17        }
18        else {
19            context.Reject(
20                error: OpenIdConnectConstants.Errors.InvalidRequest,
21                description: "User isn't valid"
22            );
23            return Task.CompletedTask;
24        }
25
26    }
27    // Catch all error
28    context.Reject(
29        error: OpenIdConnectConstants.Errors.ServerError,
30        description: "Could not validate the token request"
31    );
32    return Task.CompletedTask;
33}

Something to pay attention to are lines 7 and 13. If performing a client credentials grant, we have to supply the authentication ticket ourselves here in this method - but if this incoming request is for one of the other flows, then the ticket is already available on the context (at least it should be, which is why we have the null check) - this behavior is implemented at a later section, when we cover implementing the Views for the authorization flows.

Apply Token Response

Once a given token flow has succeeded and been serialized into the response by ASOS, we need to write the new tokens to our database. We take note of the token type, the grant type, and the raw value, then we send it off to our not-yet-existent TokenService to be written to the database.

// Our Token Request was successful - we should write the returned values to the database.
public override async Task ApplyTokenResponse(ApplyTokenResponseContext context) {
    if (context.Error != null) {
        return;
    }
    TService = context.HttpContext.RequestServices.GetRequiredService<TokenService>();
    ApplicationDbContext dbContext = context.HttpContext.RequestServices.GetRequiredService<ApplicationDbContext>();
    OAuthClient client = await dbContext.ClientApplications.FirstOrDefaultAsync(x => x.ClientId == context.Request.ClientId);
    if (client == null) {
        return;
    }

    // Implicit Flow Tokens are not returned from the `Token` group of methods - you can find them in the `Authorize` group.
    if (context.Request.IsClientCredentialsGrantType()) {
        // The only thing returned from a successful client grant is a single `Token`
        Token t = new Token() {
            TokenType = OpenIdConnectConstants.TokenUsages.AccessToken,
            GrantType = OpenIdConnectConstants.GrantTypes.ClientCredentials,
            Value = context.Response.AccessToken,
        };

        await TService.WriteNewTokenToDatabase(context.Request.ClientId, t);
    }
    else if (context.Request.IsAuthorizationCodeGrantType()) {
        Token access = new Token() {
            TokenType = OpenIdConnectConstants.TokenUsages.AccessToken,
            GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
            Value = context.Response.AccessToken,
        };
        Token refresh = new Token() {
            TokenType = OpenIdConnectConstants.TokenUsages.RefreshToken,
            GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
            Value = context.Response.RefreshToken,
        };

        await TService.WriteNewTokenToDatabase(context.Request.ClientId, access, context.Ticket.Principal);
        await TService.WriteNewTokenToDatabase(context.Request.ClientId, refresh, context.Ticket.Principal);
    }
    else if (context.Request.IsRefreshTokenGrantType()) {
        Token access = new Token() {
            TokenType = OpenIdConnectConstants.TokenUsages.AccessToken,
            GrantType = OpenIdConnectConstants.GrantTypes.AuthorizationCode,
            Value = context.Response.AccessToken,
        };
        await TService.WriteNewTokenToDatabase(context.Request.ClientId, access, context.Ticket.Principal);
    }
}

Authorization Code grants supply both a Refresh and an Access token, so we need to account for both those. The other grants just return a single access token. Remember though that in some implementations, including perhaps your own, a refresh request might return another refresh token in addition to a new access token - be sure to adjust for what your implementation requirements are.

Moving On

The demo of this project to this point can be found here on GitHub.

In the next section we’ll implement the TokenService and ValidationService classes that we’ve been referencing.
Next

Posts in this series