Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Identity Server 4 Authorization Code Flow example

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?

like image 795
Rafael Miceli Avatar asked May 18 '16 20:05

Rafael Miceli


People also ask

What is authorization code in Identity server 4?

The Authorization Server validates the user credentials and provides with an Authorization Code. An Authorization Code is a temporary code generated by the Authorization Server representing the authenticated user, which the client exchanges for a token.

What is authorization code grant flow?

4.1. The authorization code is a temporary code that the client will exchange for an access token. The code itself is obtained from the authorization server where the user gets a chance to see what the information the client is requesting, and approve or deny the request.

What is AllowedGrantTypes?

Grant types specify how a client can interact with the token service. You need to specify which grant types a client can use via the AllowedGrantTypes property on the Client configuration. This allows locking down the protocol interactions that are allowed for a given client.


2 Answers

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

like image 163
bayardw Avatar answered Sep 17 '22 18:09

bayardw


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

like image 22
leastprivilege Avatar answered Sep 20 '22 18:09

leastprivilege