/// <summary> /// Validates the token request. /// </summary> /// <param name="context">Class describing the extension grant validation context</param> public async Task ValidateAsync(ExtensionGrantValidationContext context) { var userToken = context.Request.Raw.Get("token"); var code = context.Request.Raw.Get("code"); var providerName = context.Request.Raw.Get("provider"); var provider = default(TotpProviderType?); var reason = context.Request.Raw.Get("reason"); if (string.IsNullOrEmpty(userToken)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Access token 'token' parameter missing from payload."); return; } if (string.IsNullOrEmpty(code)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Payload must provide a valid totp 'code'."); return; } var validationResult = await _validator.ValidateAccessTokenAsync(userToken); if (validationResult.IsError) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Access token validation failed"); return; } if (!string.IsNullOrEmpty(providerName)) { if (Enum.TryParse <TotpProviderType>(providerName, true, out var pv)) { provider = pv; } else { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest, "Unsupported 'provider'."); return; } } // Get user's identity. var sub = validationResult.Claims.FirstOrDefault(x => x.Type == "sub").Value; var user = new ClaimsPrincipal(new ClaimsIdentity(validationResult.Claims)); var totpResult = await _totpService.Verify(user, code, provider, reason); if (!totpResult.Success) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant); return; } context.Result = new GrantValidationResult(sub, GrantType); return; }
/// <inheritdoc /> public async Task ValidateAsync(ExtensionGrantValidationContext context) { var rawRequest = context.Request.Raw; var accessToken = rawRequest.Get("token"); /* 1. Check if an access token exists in the request. */ if (string.IsNullOrWhiteSpace(accessToken)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Access token not present in the request."); return; } /* 2. Validate given access token. */ var tokenValidationResult = await _tokenValidator.ValidateAccessTokenAsync(accessToken); if (tokenValidationResult.IsError) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Access token is not valid."); return; } /* 3. Check if given access token contains a subject. */ var subject = tokenValidationResult.Claims.FirstOrDefault(x => x.Type == JwtClaimTypes.Subject).Value; if (string.IsNullOrWhiteSpace(subject)) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Claim 'sub' was not found."); return; } /* 4. Query the store for a user with an id equal to the one found in the token. */ var user = await _userManager.FindByIdAsync(subject); if (user == null || !user.PhoneNumberConfirmed || user.Blocked) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant); return; } /* 5. Check if an OTP is provided in the request. */ var purpose = $"{TotpConstants.TokenGenerationPurpose.SessionOtp}:{user.Id}"; var otp = rawRequest.Get("otp"); var principal = Principal.Create("OtpAuthenticatedUser", new List <Claim> { new Claim(JwtClaimTypes.Subject, subject) } .ToArray()); /* 5.1 If an OTP is not provided, then we must send one to the user's confirmed phone number. */ if (string.IsNullOrWhiteSpace(otp)) { /* 5.1.1 In order to send the OTP we have to decide the delivery channel. Delivery channel can optionally be sent in the request. */ var providedChannel = rawRequest.Get("channel"); var channel = TotpDeliveryChannel.Sms; if (!string.IsNullOrWhiteSpace(providedChannel)) { if (Enum.TryParse <TotpDeliveryChannel>(providedChannel, ignoreCase: true, out var parsedChannel)) { channel = parsedChannel; } else { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Invalid delivery channel."); return; } } await _totpService.Send(builder => builder.UsePrincipal(principal) .WithMessage(_identityMessageDescriber.OtpSecuredValidatorOtpBody()) .ToPhoneNumber(user.PhoneNumber) .UsingDeliveryChannel(channel, _identityMessageDescriber.OtpSecuredValidatorOtpSubject) .WithPurpose(purpose)); context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "An OTP code was sent to the user. Please replay the request and include the verification code.", new Dictionary <string, object> { { "otp_sent", true } }); return; } /* 5.2 If an OTP is provided, then we must verify it at first. */ var totpVerificationResult = await _totpService.Verify(principal, otp, purpose : purpose); /* If OTP verification code is not valid respond accordingly. */ if (!totpVerificationResult.Success) { context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "OTP verification code could not be validated."); return; } /* If OTP verification code is valid add the same claims that were present in the token and a new one to mark that OTP verification has been successfully completed. */ var claims = tokenValidationResult.Claims.ToList(); claims.Add(new Claim(BasicClaimTypes.OtpAuthenticated, "true", ClaimValueTypes.Boolean)); context.Result = new GrantValidationResult(subject, GrantType, claims); }
/// <summary> /// Verify the code received for the given user id. /// </summary> /// <param name="service">The service to use.</param> /// <param name="userId">The user id.</param> /// <param name="code">The TOTP code.</param> /// <param name="provider">Optionaly pass the provider to use to verify. Defaults to DefaultPhoneProvider</param> /// <param name="reason">Optionaly pass the reason used to generate the TOTP.</param> public static Task <TotpResult> Verify(this ITotpService service, string userId, string code, TotpProviderType?provider = null, string reason = null) => service.Verify(new ClaimsPrincipal(new ClaimsIdentity(new[] { new Claim(BasicClaimTypes.Subject, userId) })), code, provider, reason);