I may be going about this all wrong, but I have been following the sample setup in this article: Creating your own OpenID Connect server with ASOS, however, it does not explain how to implement two factor as that is not the primary purpose of this article.
The only mention of two faction in this article is in the token endpoint where there is a check for two factor authentication where it rejects the request for a token if two factor is enabled. So I added a check in my client application that checks for that specific reject condition and then redirects to a page that asks for the two factor code. This page first calls the authorize endpoint which sends the code to the user's phone that is on file. Then once the user has entered the code, it calls the authorize endpoint again with that code and the method that handles the authorize endpoint validates the code and returns a SignInResult if successful. However, I am getting a 500 error in the SignIn(principle, properties, schema) method that returns the SignInResult.
First of all, I would like to know if I am doing this right, and second, if I am doing it correctly, what am I doing wrong? Please find my relevant bits of code below:
AuthorizationProvider:
public override async Task HandleTokenRequest(HandleTokenRequestContext context) {
var _dbContext = context.HttpContext.RequestServices.GetRequiredService<DbContext>();
if(context.Request.IsPasswordGrantType()) {
var u = await _dbContext.Users.Include(m => m.Organization).SingleOrDefaultAsync(x => x.Email.ToLowerInvariant() == context.Request.Username.ToLowerInvariant());
if(u == null || !PasswordHelper.VerifyPassword(context.Request.Password, u.Password)) {
context.Reject(
error: OpenIdConnectConstants.Errors.AccessDenied,
description: "The email or password is incorrect");
return;
} else if(u.Organization == null) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Your user account is not associated with an organization.");
return;
}
// // Reject the token request if two-factor authentication has been enabled by the user.
if(u.TwoFactorEnabled) {
context.Reject(
error: OpenIdConnectConstants.Errors.InvalidGrant,
description: "Two-factor authentication is required for this account.");
return;
}
var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, u.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, context.Request.Username));
claims.Add(new Claim(ClaimTypes.Role, u.RoleString));
claims.Add(new Claim("user_id", u.Id.ToString()));
claims.Add(new Claim("org_id", u.Organization.Id.ToString()));
foreach(var claim in claims) {
claim.SetDestinations(new List<string> { OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken });
}
identity.AddClaims(claims);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
context.Options.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile,
/* offline_access: */ OpenIdConnectConstants.Scopes.OfflineAccess);
context.Validate(ticket);
}
}
Authorize Method:
[HttpPost]
[Route("authorize")]
public async Task<IActionResult> Authorize() {
var request = HttpContext.GetOpenIdConnectRequest();
if(request.Code == null) {
var u = await _dbContext.Users.SingleOrDefaultAsync(x => x.Email.ToLowerInvariant() == request.Username.ToLowerInvariant());
if(u == null || !PasswordHelper.VerifyPassword(request.Password, u.Password)) {
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
var response = await _twoFactorProvider.SendCode(CodeMethods.sms, u.AuthyId);
return Ok(new { Id = u.AuthyId, Response = response });
} else {
var response = await _twoFactorProvider.VerifyCode(request.Code, int.Parse(request.Username));
if(response.Token != "is valid") {
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
var u = await _dbContext.Users.Include(m => m.Organization).SingleOrDefaultAsync(x => x.AuthyId == int.Parse(request.Username));
var identity = new ClaimsIdentity(OpenIdConnectServerDefaults.AuthenticationScheme);
List<Claim> claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.NameIdentifier, u.Id.ToString()));
claims.Add(new Claim(ClaimTypes.Name, u.Email));
claims.Add(new Claim(ClaimTypes.Role, u.RoleString));
claims.Add(new Claim("user_id", u.Id.ToString()));
claims.Add(new Claim("org_id", u.Organization.Id.ToString()));
foreach(var claim in claims) {
claim.SetDestinations(new List<string> { OpenIdConnectConstants.Destinations.AccessToken, OpenIdConnectConstants.Destinations.IdentityToken });
}
identity.AddClaims(claims);
var ticket = new AuthenticationTicket(
new ClaimsPrincipal(identity),
new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
ticket.SetScopes(
/* openid: */ OpenIdConnectConstants.Scopes.OpenId,
/* email: */ OpenIdConnectConstants.Scopes.Email,
/* profile: */ OpenIdConnectConstants.Scopes.Profile,
/* offline_access: */ OpenIdConnectConstants.Scopes.OfflineAccess);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
}
Client Methods:
login(email, password) {
var deferred = q.defer();
var payload = {
'grant_type': 'password',
'username': email,
'password': password,
'scope': 'openid offline_access'
};
var url = this.authContextConfiguration.baseUrl + 'connect/token';
$.ajax({
url: url,
type: 'POST',
contentType: 'application/x-www-form-urlencoded',
data: payload,
success: function (data) {
// store the access token / everything else //
deferred.resolve();
},
error: function (req) {
var error = {
message: req.responseJSON.error_description || 'There was an error when signing in.'
};
deferred.reject(error);
}
});
return deferred.promise;
}
twoFactorLogin(email, password) {
var deferred = q.defer();
var payload = {
'username': email,
'password': password
}
var url = this.authContextConfiguration.baseUrl + 'connect/authorize';
$.ajax({
url: url,
type: 'POST',
contentType: 'application/x-www-form-urlencoded',
data: payload,
success: function (data) {
deferred.resolve(data);
},
error: function (req) {
var error = {
message: req.responseJSON.error_description || 'There was an error generating your two factor code.'
};
deferred.reject(error);
}
});
return deferred.promise;
}
verifyTwoFactorCode(id, code, remember) {
var deferred = q.defer();
var payload = {
'username': id,
'code': code,
'rememberMe': remember
}
var url = this.authContextConfiguration.baseUrl + 'connect/authorize';
$.ajax({
url: url,
type: 'POST',
contentType: 'application/x-www-form-urlencoded',
data: payload,
success: function (data) {
deferred.resolve(data);
},
error: function (req) {
var error = {
message: req.responseJSON.error_description || 'There was an error verifying your two factor code.'
};
deferred.reject(error);
}
});
return deferred.promise;
}