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?
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, "user@somesecurecompany.com"), 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