OAuth2.0 Learning Note. Authorization Code Grant
Learning Note About Web Authentication and Authorize
1.we use Owin to implement the Authentication and Authorize.
we create a new Startup.cs file to replace the global.asax file. here is a general content of the startup.cs file.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Security.OAuth;
using angularjsAuthentication.api.Providers;
[assembly:OwinStartup(typeof(angularjsAuthentication.api.Startup))]
namespace angularjsAuthentication.api
{
public class Startup
{
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
app.UseWebApi(config);
}
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider()
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
}
}
- Some key class : OAuthAuthorizationServerOptions
OAuthAuthorizationServerProvider
we have an important interface IOAuthAuthorizationServerProvider, the OAuthAuthorizationServerOptions provide a default implementation of this interface.
if we have any custom requirement, we can inherite it and override some methods.
2.1 For the first method OAuthAuthorizationServerProvider.ValidateClientAuthentication(), the key point, if validate pass, call context.Validate(), otherwise, call context.setErrors().
2.2 For this class, take care of these methods OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials
this method is reponsible for grantting access token to the request with grant_type as password, if success, call context.validate(token). generally, if a request arrives at token Endpoint with grant_type password, this method will be called.
these sub class AuthenticationTicket, ClaimsIdentity. AuthenticationProperties
2.3 OAuthAuthorizationServerProvider.GrantRefreshToken, called when a request to tokenendpoint with grant_type refresh_token. we can see the http api.
3. OAuth2
3.1 we have a lot of high quality articles descriping this protocol, here is just a link: link1, we can get a lot from cnblogs.
here we just make things simple, OAuth2 support four types of Authorization granttypes: Authorization Code Grant, Implicit Grant, Resource Owener Password Credentials Grant, Client Credential Grant. For each Authorization granttype, we make a note of each method called during a end2end test.
3.1 Resource Owener Password Credentials Grant
first, we request the access token, this method will be called OAuthAuthorizationServerProvider.ValidateClientAuthentication
, this function is called to validate if the client is a registered client. if passed, OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials
will be called. see the msdn .
secondly, if we provide the RefreshTokenProvider which implete the interface IAuthenticationTokenProvider, if user request an access token, the workflow will show like this: OAuthAuthorizationServerProvider.ValidateClientAuthentication -> OAuthAuthorizationServerProvider.GrantResourceOwnerCredentials -> IAuthenticationTokenProvider.CreateAsync -> OAuthAuthorizationServerProvider.TokenEndpoint; if user try to refresh the access token, the workflow will like this: OAuthAuthorizationServerProvider.ValidateClientAuthentication ->
OAuthAuthorizationServerProvider.GrantRefreshToken -> IAuthenticationTokenProvider.ReceiveAsync -> OAuthAuthorizationServerProvider.TokenEndpoint
3.2 Authorization Code Grant
For the detail workflow we have a great doc. Here is a general sample code.
Startup.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using Microsoft.Owin;
using Owin;
using Microsoft.Owin.Security.OAuth;
using angularjsAuthentication.api.Providers;
using System.Data.Entity;
using angularjsAuthentication.api.DataRepository;
using Microsoft.Owin.Security.Infrastructure;
using System.Collections.Concurrent;
using log4net;
using System.Web.Routing;
[assembly:OwinStartup(typeof(angularjsAuthentication.api.Startup))]
namespace angularjsAuthentication.api
{
public class Startup
{
private static ILog Log = LogManager.GetLogger(typeof(Startup));
public void Configuration(IAppBuilder app)
{
HttpConfiguration config = new HttpConfiguration();
ConfigureOAuth(app);
WebApiConfig.Register(config);
RouteConfig.RegisterRoutes(RouteTable.Routes);
app.UseWebApi(config);
Database.SetInitializer(new configuration());
}
private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AuthorizeEndpointPath = new PathString("/OAuth/Authorize"),
AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
Provider = new SimpleAuthorizationServerProvider(),
RefreshTokenProvider = new SimpleRefreshTokenProvider(),
AuthorizationCodeProvider= new AuthenticationTokenProvider()
{
OnCreate = CreateAuthenticationCode,
OnReceive = ReceiveAuthenticationCode
}
};
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
}
private void CreateAuthenticationCode(AuthenticationTokenCreateContext context)
{
// context.SetToken(Guid.NewGuid().ToString("n") + Guid.NewGuid().ToString("n"));
context.SetToken("123");
_authenticationCodes[context.Token] = context.SerializeTicket();
Log.Info("Create a token with value: 123");
}
private void ReceiveAuthenticationCode(AuthenticationTokenReceiveContext context)
{
string value;
if(_authenticationCodes.TryRemove(context.Token,out value))
{
context.DeserializeTicket(value);
Log.Info("Call at ReceiveAuthenticationCode");
}
}
}
}
SimpleAuthorizationServerProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.OAuth;
using System.Threading.Tasks;
using angularjsAuthentication.api.Entities;
using angularjsAuthentication.api.DataRepository;
using Microsoft.AspNet.Identity.EntityFramework;
using System.Security.Claims;
using log4net;
namespace angularjsAuthentication.api.Providers
{
public class SimpleAuthorizationServerProvider:OAuthAuthorizationServerProvider
{
private static ILog Log = LogManager.GetLogger(typeof(SimpleAuthorizationServerProvider));
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
string clientId = string.Empty;
string clientSecret = string.Empty;
if(!context.TryGetBasicCredentials(out clientId,out clientSecret))
{
context.TryGetFormCredentials(out clientId, out clientSecret);
}
Log.InfoFormat("SimpleAuthorizationServerProvider.ValidateClientAuthentication, client id: {0},client secrect {1}", clientId, clientSecret);
context.Validated();
return Task.FromResult<object>(null);
//if (clientId==null)
//{
// context.SetError("invalid_clientId", "clientId should be present.");
// return Task.FromResult<object>(null);
//}
//using (AuthRepository _repo = new AuthRepository())
//{
// client = _repo.FindClient(clientId);
//}
//if(client==null)
//{
// context.SetError("invalid_clientId", string.Format("Client '{0}' is not registered in the system.", context.ClientId));
//}
//if(client.ApplicatonType==Models.ApplicationTypes.NativeConfidential)
//{
// if(string.IsNullOrWhiteSpace(clientSecret))
// {
// context.SetError("invalid_clientId", "Client secret should be sent.");
// return Task.FromResult<object>(null);
// }
// else
// {
// if(client.secrect!=Util.Help.GetHash(clientSecret))
// {
// context.SetError("invalid_clientId", "Client is inactive.");
// return Task.FromResult<object>(null);
// }
// }
//}
//if(!client.Active)
//{
// context.SetError("invalid_clientId", "Client is inactive.");
// return Task.FromResult<object>(null);
//}
//context.OwinContext.Set<string>("as:clientAllowedOrigin", client.AllowedOrigin);
//context.OwinContext.Set<string>("as:clientRefreshTokenLifeTime", client.RefreshTokenLiftTime.ToString());
//context.Validated();
//return Task.FromResult<object>(null);
}
public override Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
if (allowedOrigin == null) allowedOrigin = "*";
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
//using (AuthRepository _repo = new AuthRepository())
//{
// IdentityUser user = await _repo.FindUser(context.UserName, context.Password);
// if(user==null)
// {
// context.SetError("invalid_grant", "The user name of password is incorrect");
// return;
// }
//}
Log.InfoFormat("SimpleAuthorizationServerProvider.GrantResourceOwnerCredentials with userName: {0}, Password: {1}", context.UserName, context.Password);
var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
identity.AddClaim(new Claim("sub", context.UserName));
var props = new AuthenticationProperties(new Dictionary<string, string> {
{
"as:client_id",(context.ClientId==null)?string.Empty:context.ClientId
},
{
"userName",context.UserName
}
});
var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
return Task.FromResult<object>(null);
}
public override Task ValidateClientRedirectUri(OAuthValidateClientRedirectUriContext context)
{
Log.InfoFormat("Call at SimpleAuthorizationServerProvider.ValidateClientRedirectUri");
context.Validated("https://www.getpostman.com/oauth2/callback");
return Task.FromResult<object>(null);
}
public override Task AuthorizeEndpoint(OAuthAuthorizeEndpointContext context)
{
Log.InfoFormat("grant the authorization directly in AuthorizeEndpoint");
//var identity = new ClaimsIdentity(context.Options.AuthenticationType);
//identity.AddClaim(new Claim(ClaimTypes.Name, "Name"));
//identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
//identity.AddClaim(new Claim("sub", "sub"));
//context.OwinContext.Authentication.SignIn(identity);
Log.Info("grant success");
return Task.FromResult<object>(null);
}
public override Task GrantRefreshToken(OAuthGrantRefreshTokenContext context)
{
var originalClient = context.Ticket.Properties.Dictionary["as:client_id"];
var currentClient = context.ClientId;
if(originalClient!=currentClient)
{
context.SetError("invalid_clientId", "Refresh token is issued to a different clientId");
return Task.FromResult<object>(null);
}
var newIdentity = new ClaimsIdentity(context.Ticket.Identity);
var newClaim = newIdentity.Claims.FirstOrDefault(c => c.Type == "newClaim");
if(newClaim!=null)
{
newIdentity.RemoveClaim(newClaim);
}
newIdentity.AddClaim(new Claim("newClaim", "newValue"));
var newTicket = new AuthenticationTicket(newIdentity, context.Ticket.Properties);
context.Validated(newTicket);
return Task.FromResult<object>(null);
}
public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
context.Properties.Dictionary.ToList()
.ForEach(e =>
{
context.AdditionalResponseParameters.Add(e.Key, e.Value);
});
return Task.FromResult<object>(null);
}
public override Task GrantAuthorizationCode(OAuthGrantAuthorizationCodeContext context)
{
Log.Info("Call in GrantAuthorizationCode");
var ticket = context.Ticket;
context.Validated(ticket);
return Task.FromResult<object>(null);
}
}
}
OAuthrize.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Security.Claims;
using log4net;
namespace angularjsAuthentication.api.Controllers.OAuth
{
public class OAuthController : Controller
{
private static ILog Log = LogManager.GetLogger(typeof(OAuthController));
// GET: OAuth
public ActionResult Index()
{
return View();
}
public ActionResult Authorize()
{
var authentication = HttpContext.GetOwinContext().Authentication;
if (Request.HttpMethod == "POST")
{
ClaimsIdentity identity = new ClaimsIdentity("Bearer", "myOAuthNameClaim", "myOAuthRoleClaim");
identity.AddClaim(new Claim("Scope", "Name"));
authentication.SignIn(identity);
}
Log.Info("Call at OAuthController.Authorize, create Identity with authentication type: myOAuthAuthentication, Name Claim: myOAuthNameClaim, Role Claim: myOAuthRoleClaim ");
return View("Authroze");
}
}
}
SimpleRefreshTokenProvider.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using angularjsAuthentication.api.Entities;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using System.Threading.Tasks;
using angularjsAuthentication.api.DataRepository;
using angularjsAuthentication.api.Util;
using System.Collections.Concurrent;
namespace angularjsAuthentication.api.Providers
{
public class SimpleRefreshTokenProvider:IAuthenticationTokenProvider
{
private readonly ConcurrentDictionary<string, string> _authenticationCodes = new ConcurrentDictionary<string, string>(StringComparer.Ordinal);
public Task CreateAsync(AuthenticationTokenCreateContext context)
{
//var clientId = context.Ticket.Properties.Dictionary["as:client_id"];
//if (string.IsNullOrEmpty(clientId))
// return;
var refreshTokenId = Guid.NewGuid().ToString("n");
var token = new RefreshToken()
{
Id = Help.GetHash(refreshTokenId),
ClientId = "testClient",
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpireUtc = DateTime.UtcNow.AddMinutes(100)
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpireUtc;
token.ProtectedTicket = context.SerializeTicket();
_authenticationCodes[token.Id] = token.ProtectedTicket;
context.SetToken(refreshTokenId);
return Task.FromResult<object>(null);
//using (AuthRepository _repo = new AuthRepository())
//{
// var refreshTokenLifetime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
// var token = new RefreshToken()
// {
// Id = Help.GetHash(refreshTokenId),
// ClientId = clientId,
// Subject = context.Ticket.Identity.Name,
// IssuedUtc = DateTime.UtcNow,
// ExpireUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifetime))
// };
// context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
// context.Ticket.Properties.ExpiresUtc = token.ExpireUtc;
// token.ProtectedTicket = context.SerializeTicket();
// var result = await _repo.AddRefreshTokenAsync(token);
// if(result)
// {
// context.SetToken(refreshTokenId);
// }
//}
}
public Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{
//var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
//context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
string hashedTokenId = Help.GetHash(context.Token);
string value;
if (_authenticationCodes.TryRemove(hashedTokenId, out value))
{
context.DeserializeTicket(value);
}
return Task.FromResult<object>(null);
//using (AuthRepository _repo = new AuthRepository())
//{
// var refreshToken = await _repo.FindRefreshTokenAsync(hashedTokenId);
// if(refreshToken!=null)
// {
// context.DeserializeTicket(refreshToken.ProtectedTicket);
// var result = await _repo.RemoveRefreshTokenAsync(hashedTokenId);
// }
//}
}
public void Create(AuthenticationTokenCreateContext context)
{
var clientId = context.Ticket.Properties.Dictionary["as:client_id"];
if (string.IsNullOrEmpty(clientId))
return;
var refreshTokenId = Guid.NewGuid().ToString("n");
using (AuthRepository _repo = new AuthRepository())
{
var refreshTokenLifeTime = context.OwinContext.Get<string>("as:clientRefreshTokenLifeTime");
var token = new RefreshToken()
{
Id = Help.GetHash(refreshTokenId),
ClientId = clientId,
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpireUtc = DateTime.UtcNow.AddMinutes(Convert.ToDouble(refreshTokenLifeTime))
};
context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpireUtc;
token.ProtectedTicket = context.SerializeTicket();
var result = _repo.AddRefreshToken(token);
if (result)
context.SetToken(refreshTokenId);
}
}
public void Receive(AuthenticationTokenReceiveContext context)
{
var allowedOrigin = context.OwinContext.Get<string>("as:clientAllowedOrigin");
context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { allowedOrigin });
string hashedTokenId = Help.GetHash(context.Token);
using (AuthRepository _repo = new AuthRepository())
{
_repo.RemoveRefreshToken(hashedTokenId);
}
}
}
}
**In the OAuthrize Control, the key function call is below. Bearer is necessary.
ClaimsIdentity identity = new ClaimsIdentity("Bearer", "myOAuthNameClaim", "myOAuthRoleClaim");
identity.AddClaim(new Claim("Scope", "Name"));
authentication.SignIn(identity);