1

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;
}
light
  • 816
  • 5
  • 16
  • 1
    Looks like you're trying to use the authorization endpoint as an "API endpoint", which can't work as this endpoint is made to be used by non-API callers. Since you're trying to create your own "password grant" with 2FA support, I'd recommend creating a custom grant type exclusively using the token endpoint. Here's an example of how you can implemnt custom grant type support in ASOS: http://stackoverflow.com/questions/39008642/use-openidconnectserver-and-try-to-connect-to-facebook-through-api-service – Kévin Chalet Feb 13 '17 at 08:08
  • I had already changed it to a custom password grant using the id as the username and the two factor code as the password, but this way is a lot cleaner and easier to understand in the code. Thanks for this, I just wish you had posted it as an answer so I could accept it as the answer. – light Feb 13 '17 at 15:31
  • why not posting your own answer and marking it as the accepted one? :) – Kévin Chalet Feb 13 '17 at 16:01

0 Answers0