Identity Server 4 Authorization Code Flow example

43,835

Solution 1

Here's an implementation of an Authorization Code Flow with Identity Server 4 and an MVC client to consume it.

IdentityServer4 can use a client.cs file to register our MVC client, it's ClientId, ClientSecret, allowed grant types (Authorization Code in this case), and the RedirectUri of our client:

public class Clients
{
    public static IEnumerable<Client> Get()
    {
        var secret = new Secret { Value = "mysecret".Sha512() };

        return new List<Client> {
            new Client {
                ClientId = "authorizationCodeClient2",
                ClientName = "Authorization Code Client",
                ClientSecrets = new List<Secret> { secret },
                Enabled = true,
                AllowedGrantTypes = new List<string> { "authorization_code" }, //DELTA //IdentityServer3 wanted Flow = Flows.AuthorizationCode,
                RequireConsent = true,
                AllowRememberConsent = false,
                RedirectUris =
                  new List<string> {
                       "http://localhost:5436/account/oAuth2"
                  },
                PostLogoutRedirectUris =
                  new List<string> {"http://localhost:5436"},
                AllowedScopes = new List<string> {
                    "api"
                },
                AccessTokenType = AccessTokenType.Jwt
            }
        };
    }
}

This class is referenced in the ConfigurationServices method of the Startup.cs in the IdentityServer4 project:

    public void ConfigureServices(IServiceCollection services)
    {
        ////Grab key for signing JWT signature
        ////In prod, we'd get this from the certificate store or similar
        var certPath = Path.Combine(PlatformServices.Default.Application.ApplicationBasePath, "SscSign.pfx");
        var cert = new X509Certificate2(certPath);

        // configure identity server with in-memory stores, keys, clients and scopes
        services.AddDeveloperIdentityServer(options =>
            {
                options.IssuerUri = "SomeSecureCompany";
            })
            .AddInMemoryScopes(Scopes.Get())
            .AddInMemoryClients(Clients.Get())
            .AddInMemoryUsers(Users.Get())
            .SetSigningCredential(cert);

        services.AddMvc();
    }

For reference, here are the Users and Scopes classes referenced above:

public static class Users
{
    public static List<InMemoryUser> Get()
    {
        return new List<InMemoryUser> {
            new InMemoryUser {
                Subject = "1",
                Username = "user",
                Password = "pass123",
                Claims = new List<Claim> {
                    new Claim(ClaimTypes.GivenName, "GivenName"),
                    new Claim(ClaimTypes.Surname, "surname"), //DELTA //.FamilyName in IdentityServer3
                    new Claim(ClaimTypes.Email, "[email protected]"),
                    new Claim(ClaimTypes.Role, "Badmin")
                }
            }
        };
    }
}

public class Scopes
{
    // scopes define the resources in your system
    public static IEnumerable<Scope> Get()
    {
        return new List<Scope> {
            new Scope
            {
                Name = "api",
                DisplayName = "api scope",
                Type = ScopeType.Resource,
                Emphasize = false,
            }
        };
    }
}

The MVC application requires two controller methods. The first method kicks-off the Service Provider (SP-Initiated) workflow. It creates a State value, saves it in cookie-based authentication middleware, and then redirects the browser to the IdentityProvider (IdP) - our IdentityServer4 project in this case.

public ActionResult SignIn()
{
    var state = Guid.NewGuid().ToString("N");

    //Store state using cookie-based authentication middleware
    this.SaveState(state);

    //Redirect to IdP to get an Authorization Code
    var url = idPServerAuthUri +
        "?client_id=" + clientId +
        "&response_type=" + response_type +
        "&redirect_uri=" + redirectUri +
        "&scope=" + scope +
        "&state=" + state;

    return this.Redirect(url); //performs a GET
}

For reference, here are the constants and SaveState method utilized above:

//Client and workflow values
private const string clientBaseUri = @"http://localhost:5436";
private const string validIssuer = "SomeSecureCompany";
private const string response_type = "code";
private const string grantType = "authorization_code";

//IdentityServer4
private const string idPServerBaseUri = @"http://localhost:5000";
private const string idPServerAuthUri = idPServerBaseUri + @"/connect/authorize";
private const string idPServerTokenUriFragment = @"connect/token";
private const string idPServerEndSessionUri = idPServerBaseUri + @"/connect/endsession";

//These are also registered in the IdP (or Clients.cs of test IdP)
private const string redirectUri = clientBaseUri + @"/account/oAuth2";
private const string clientId = "authorizationCodeClient2";
private const string clientSecret = "mysecret";
private const string audience = "SomeSecureCompany/resources";
private const string scope = "api";


//Store values using cookie-based authentication middleware
private void SaveState(string state)
{
    var tempId = new ClaimsIdentity("TempCookie");
    tempId.AddClaim(new Claim("state", state));

    this.Request.GetOwinContext().Authentication.SignIn(tempId);
}

The second MVC action method is called by IdenityServer4 after the user enters their credentials and checks any authorization boxes. The action method:

  • Grabs the Authorization Code and State from the query string
  • Validates State
  • POSTs back to IdentityServer4 to exchange the Authorization Code for an Access Token

Here's the method:

[HttpGet]
public async Task<ActionResult> oAuth2()
{
    var authorizationCode = this.Request.QueryString["code"];
    var state = this.Request.QueryString["state"];

    //Defend against CSRF attacks http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html
    await ValidateStateAsync(state);

    //Exchange Authorization Code for an Access Token by POSTing to the IdP's token endpoint
    string json = null;
    using (var client = new HttpClient())
    {
        client.BaseAddress = new Uri(idPServerBaseUri);
        var content = new FormUrlEncodedContent(new[]
        {
                new KeyValuePair<string, string>("grant_type", grantType)
            ,new KeyValuePair<string, string>("code", authorizationCode)
            ,new KeyValuePair<string, string>("redirect_uri", redirectUri)
            ,new KeyValuePair<string, string>("client_id", clientId)              //consider sending via basic authentication header
            ,new KeyValuePair<string, string>("client_secret", clientSecret)
        });
        var httpResponseMessage = client.PostAsync(idPServerTokenUriFragment, content).Result;
        json = httpResponseMessage.Content.ReadAsStringAsync().Result;
    }

    //Extract the Access Token
    dynamic results = JsonConvert.DeserializeObject<dynamic>(json);
    string accessToken = results.access_token;

    //Validate token crypto
    var claims = ValidateToken(accessToken);

    //What is done here depends on your use-case. 
    //If the accessToken is for calling a WebAPI, the next few lines wouldn't be needed. 

    //Build claims identity principle
    var id = new ClaimsIdentity(claims, "Cookie");              //"Cookie" matches middleware named in Startup.cs

    //Sign into the middleware so we can navigate around secured parts of this site (e.g. [Authorized] attribute)
    this.Request.GetOwinContext().Authentication.SignIn(id);

    return this.Redirect("/Home"); 
}

Checking that the State received is what you expected helps defend against CSRF attacks: http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html

This ValidateStateAsync method compares the received State to what was saved off in the cookie middleware:

private async Task<AuthenticateResult> ValidateStateAsync(string state)
{
    //Retrieve state value from TempCookie
    var authenticateResult = await this.Request
        .GetOwinContext()
        .Authentication
        .AuthenticateAsync("TempCookie");

    if (authenticateResult == null)
        throw new InvalidOperationException("No temp cookie");

    if (state != authenticateResult.Identity.FindFirst("state").Value)
        throw new InvalidOperationException("invalid state");

    return authenticateResult;
}

This ValidateToken method uses Microsoft's System.IdentityModel and System.IdentityModel.Tokens.Jwt libraries to check that JWT is properly signed.

private IEnumerable<Claim> ValidateToken(string token)
{
    //Grab certificate for verifying JWT signature
    //IdentityServer4 also has a default certificate you can might reference.
    //In prod, we'd get this from the certificate store or similar
    var certPath = Path.Combine(Server.MapPath("~/bin"), "SscSign.pfx");
    var cert = new X509Certificate2(certPath);
    var x509SecurityKey = new X509SecurityKey(cert);

    var parameters = new TokenValidationParameters
    {
        RequireSignedTokens = true,
        ValidAudience = audience,
        ValidIssuer = validIssuer,
        IssuerSigningKey = x509SecurityKey,
        RequireExpirationTime = true,
        ClockSkew = TimeSpan.FromMinutes(5)
    };

    //Validate the token and retrieve ClaimsPrinciple
    var handler = new JwtSecurityTokenHandler();
    SecurityToken jwt;
    var id = handler.ValidateToken(token, parameters, out jwt);

    //Discard temp cookie and cookie-based middleware authentication objects (we just needed it for storing State)
    this.Request.GetOwinContext().Authentication.SignOut("TempCookie");

    return id.Claims;
}

A working solution containing these source files resides on GitHub at https://github.com/bayardw/IdentityServer4.Authorization.Code

Solution 2

Here's a sample - it is using hybrid flow instead of code flow. But hybrid flow is more recommended anyways if you client library supports it (and the aspnetcore middleware does).

https://github.com/IdentityServer/IdentityServer4/tree/master/samples/Quickstarts/5_HybridFlowAuthenticationWithApiAccess

Share:
43,835
Rafael Miceli
Author by

Rafael Miceli

I am known as a software developer expert in web applications and unit tests, who loves to learn and to teach. I am an MCSD certificated with 6 years of experience building large web applications. Also I am a speaker and a blogger who loves to share knowledge. Passionate by Test Driven Development (T.D.D.) with C#, Java or Python, and I also love to build projects using git or mercurial and Continuous Integration.

Updated on November 12, 2020

Comments

  • Rafael Miceli
    Rafael Miceli over 3 years

    I'm trying to implement Identity Server 4 with AspNet Core using Authorization Code Flow.

    The thing is, the IdentityServer4 repository on github have several samples, but none with Authorization Code Flow.

    Does anyone have a sample on how to implement Authorization Code Flow with Identity Server 4 and a Client in MVC consuming it?

  • Baum mit Augen
    Baum mit Augen over 7 years
    Your link is broken. That's why you should put all the important parts in the answer itself.
  • Adrian
    Adrian about 7 years
    Something I'm having a lot of difficulty with is it seems all the tutorials / answers seem to be using some older form of IdentityServer. I know this is a question directly aimed at version 4; however, even on their documentation page there are discrepancies. For example, they no longer have AddInMemoryScopes, AddImMemoryUsers, and no standard User class. docs.identityserver.io/en/release/configuration/startup.html
  • DavidG
    DavidG almost 7 years
    Hi Dominick, I assume this is a better link to use now?
  • Rikki
    Rikki over 5 years
    Could you explain why hybrid flow is more recommended, or at least reference that claim?
  • leastprivilege
    leastprivilege over 5 years
    Because it protects against code cut'n paste attacks by including the hash of the code inside the id_token.
  • Frostless
    Frostless over 5 years
    The repo is kinda depreciated. Is it possible to have it updated to be compatible with .net core 2.0 and the latest release of Identity server 4?
  • kovac
    kovac about 5 years
    Link is broken.
  • EM0
    EM0 almost 5 years
    Updated link is broken.
  • anon
    anon almost 5 years
    @leastprivilege - You said "Because it protects against code cut'n paste attacks...." For web apps, is there any advantage to using the code flow with PKCE over the hybrid flow?
  • Gabsch
    Gabsch almost 4 years
    Link to commit (doesn't break)