-
Notifications
You must be signed in to change notification settings - Fork 163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding authenticator enrollment and verification to authentication client #695
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -4,6 +4,7 @@ | |||||
using System; | ||||||
using System.Collections.Generic; | ||||||
using System.Dynamic; | ||||||
using System.Linq; | ||||||
using System.Net.Http; | ||||||
using System.Threading; | ||||||
using System.Threading.Tasks; | ||||||
|
@@ -350,7 +351,30 @@ await AssertIdTokenValidIfExisting(response.IdToken, request.ClientId, request.S | |||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public async Task<MfaOobTokenResponse> GetTokenAsync(MfaOobTokenRequest request, CancellationToken cancellationToken = default) | ||||||
{ | ||||||
if (request == null) | ||||||
{ | ||||||
throw new ArgumentNullException(nameof(request)); | ||||||
} | ||||||
|
||||||
var body = new Dictionary<string, string>() | ||||||
{ | ||||||
{ "grant_type", "http://auth0.com/oauth/grant-type/mfa-oob" }, | ||||||
{ "client_id", request.ClientId }, | ||||||
{ "client_secret", request.ClientSecret }, | ||||||
{ "mfa_token", request.MfaToken}, | ||||||
{ "oob_code", request.OobCode}, | ||||||
{ "binding_code", request.BindingCode} | ||||||
}; | ||||||
czf marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||
return await connection.SendAsync<MfaOobTokenResponse>( | ||||||
HttpMethod.Post, | ||||||
tokenUri, | ||||||
body, | ||||||
cancellationToken: cancellationToken); | ||||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public Task RevokeRefreshTokenAsync(RevokeRefreshTokenRequest request, CancellationToken cancellationToken = default) | ||||||
{ | ||||||
if (request == null) | ||||||
|
@@ -494,6 +518,31 @@ public Task<PushedAuthorizationRequestResponse> PushedAuthorizationRequestAsync( | |||||
cancellationToken: cancellationToken | ||||||
); | ||||||
} | ||||||
|
||||||
/// <inheritdoc/> | ||||||
public Task<AssociateNewAuthenticatorResponse> AssociateNewAuthenticatorAsync(AssociateNewAuthenticatorRequest request, CancellationToken cancellationToken = default) | ||||||
{ | ||||||
if (request == null) | ||||||
{ | ||||||
throw new ArgumentNullException(nameof(request)); | ||||||
} | ||||||
if (!request.IsValid(out List<string> validationErrors)) | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure I understand why we are suddenly starting to do this without any context. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. removed this |
||||||
{ | ||||||
if (validationErrors.Count == 1) | ||||||
{ | ||||||
throw new InvalidOperationException(validationErrors.First()); | ||||||
} | ||||||
|
||||||
throw new InvalidOperationException(validationErrors.Aggregate((x, y) => x + "\n" + y)); | ||||||
} | ||||||
|
||||||
return connection.SendAsync<AssociateNewAuthenticatorResponse>( | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. made change |
||||||
HttpMethod.Post, | ||||||
BuildUri("mfa/associate"), | ||||||
request, | ||||||
BuildHeaders(request.Token), | ||||||
cancellationToken); | ||||||
} | ||||||
|
||||||
/// <summary> | ||||||
/// Disposes of any owned disposable resources such as a <see cref="IAuthenticationConnection"/>. | ||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
using System.Collections.Generic; | ||
using System.Linq; | ||
using Newtonsoft.Json; | ||
|
||
namespace Auth0.AuthenticationApi.Models | ||
{ | ||
public class AssociateNewAuthenticatorRequest | ||
{ | ||
[JsonIgnore] | ||
public string Token { get; set; } | ||
|
||
/// <summary>Your application's Client ID.</summary> | ||
[JsonProperty("client_id", Required = Required.Always)] | ||
public string ClientId { get; set; } | ||
|
||
/// <summary> | ||
/// A JWT containing a signed assertion with your application credentials. Required when Private Key JWT is your application authentication | ||
/// method. | ||
/// </summary> | ||
[JsonProperty("client_assertion")] | ||
public string ClientAssertion { get; set; } | ||
|
||
/// <summary> | ||
/// Your application's Client Secret. Required when the Token Endpoint Authentication Method field in your Application Settings is Post or | ||
/// Basic. | ||
/// </summary> | ||
[JsonProperty("client_secret")] | ||
public string ClientSecret { get; set; } | ||
|
||
/// <summary> | ||
/// The value is urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Required when Private Key JWT is the application authentication | ||
/// method. | ||
/// </summary> | ||
[JsonProperty("client_assertion_type")] | ||
public string ClientAssertionType { get; set; } | ||
|
||
/// <summary>The type of authenticators supported by the client. Value is an array with values "otp" or "oob".</summary> | ||
[JsonProperty("authenticator_types", Required = Required.Always)] | ||
public List<string> AuthenticatorTypes { get; set; } | ||
|
||
/// <summary> | ||
/// The type of OOB channels supported by the client. An array with values "auth0", "sms", "voice". Required if authenticator_types include | ||
/// oob. | ||
/// </summary> | ||
[JsonProperty("oob_channels")] | ||
public List<string> OobChannels { get; set; } | ||
|
||
/// <summary>The phone number to use for SMS or Voice. Required if oob_channels includes sms or voice.</summary> | ||
[JsonProperty("phone_number")] | ||
public string PhoneNumber { get; set; } | ||
|
||
internal bool IsValid(out List<string> validationErrors) | ||
{ | ||
validationErrors = new List<string>(); | ||
if (string.IsNullOrEmpty(Token)) | ||
{ | ||
validationErrors.Add($"{nameof(Token)} is required"); | ||
} | ||
|
||
if (string.IsNullOrEmpty(ClientId)) | ||
{ | ||
validationErrors.Add($"{nameof(ClientId)} is required"); | ||
} | ||
|
||
if (AuthenticatorTypes == null || AuthenticatorTypes.Count == 0) | ||
{ | ||
validationErrors.Add($"{nameof(AuthenticatorTypes)} is required"); | ||
} | ||
else if (AuthenticatorTypes.Contains("oob")) | ||
{ | ||
if (OobChannels == null || OobChannels.Count == 0) | ||
{ | ||
validationErrors.Add($"{nameof(OobChannels)} is required when {nameof(AuthenticatorTypes)} includes 'oob'"); | ||
} | ||
else if (OobChannels.Any(x => x != "auth0" && x != "sms" && x != "voice")) | ||
{ | ||
validationErrors.Add($"{nameof(OobChannels)} can only include 'auth0', 'sms', 'voice'"); | ||
} | ||
else if (OobChannels.Any(x => x == "sms" || x == "voice") && string.IsNullOrEmpty(PhoneNumber)) | ||
{ | ||
validationErrors.Add($"{nameof(PhoneNumber)} is Required when {nameof(OobChannels)} includes 'sms' or 'voice'"); | ||
} | ||
} | ||
|
||
return validationErrors.Count == 0; | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
using System.Collections.Generic; | ||
using Newtonsoft.Json; | ||
|
||
namespace Auth0.AuthenticationApi.Models | ||
{ | ||
public class AssociateNewAuthenticatorResponse | ||
czf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
[JsonProperty("oob_code")] | ||
public string OobCode { get; set; } | ||
|
||
[JsonProperty("binding_method")] | ||
public string BindingMethod { get; set; } | ||
|
||
[JsonProperty("secret")] | ||
public string Secret { get; set; } | ||
|
||
[JsonProperty("barcode_uri")] | ||
public string BarcodeUri { get; set; } | ||
|
||
[JsonProperty("authenticator_type")] | ||
public string AuthenticatorType { get; set; } | ||
|
||
[JsonProperty("oob_channel")] | ||
public string OobChannel { get; set; } | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not as mentioned in the docs, as the docs mention There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yeah I ran into it when first trying to test with postman. |
||
|
||
[JsonProperty("recovery_codes")] | ||
public List<string> RecoveryCodes { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
namespace Auth0.AuthenticationApi.Models | ||
{ | ||
public class MfaOobTokenRequest | ||
czf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
{ | ||
/// <summary> | ||
/// Your application's Client ID. | ||
/// </summary> | ||
public string ClientId { get; set; } | ||
|
||
/// <summary> | ||
/// Your application's Client Secret. | ||
/// Required when the Token Endpoint Authentication Method field at your Application Settings is Post or Basic. | ||
/// </summary> | ||
public string ClientSecret { get; set; } | ||
czf marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
/// <summary> | ||
/// The mfa_token you received from mfa_required error or access token with enroll scope and audience: https://{yourDomain}/mfa/ | ||
/// </summary> | ||
public string MfaToken { get; set; } | ||
|
||
/// <summary> | ||
/// The oob code received from the challenge request. | ||
/// </summary> | ||
public string OobCode { get; set; } | ||
|
||
/// <summary> | ||
/// A code used to bind the side channel (used to deliver the challenge) with the main channel you are using to authenticate. | ||
/// This is usually an OTP-like code delivered as part of the challenge message. | ||
/// </summary> | ||
public string BindingCode { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
using Newtonsoft.Json; | ||
|
||
namespace Auth0.AuthenticationApi.Models | ||
{ | ||
public class MfaOobTokenResponse : TokenBase | ||
{ | ||
/// <summary> | ||
/// The value of the different scopes issued in the token | ||
/// </summary> | ||
[JsonProperty("scope")] | ||
public string Scope { get; set; } | ||
|
||
/// <summary> | ||
/// The lifetime (in seconds) of the token | ||
/// </summary> | ||
[JsonProperty("expires_in")] | ||
public int ExpiresIn { get; set; } | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
using System.Collections.Generic; | ||
using System.Threading.Tasks; | ||
using Auth0.AuthenticationApi.Models; | ||
using Auth0.Tests.Shared; | ||
using FluentAssertions; | ||
using Xunit; | ||
|
||
namespace Auth0.AuthenticationApi.IntegrationTests | ||
{ | ||
public class MfaTests : TestBase | ||
{ | ||
private readonly AuthenticationApiClient _authenticationApiClient; | ||
|
||
public MfaTests() | ||
{ | ||
_authenticationApiClient = new AuthenticationApiClient(GetVariable("AUTH0_AUTHENTICATION_API_URL")); | ||
} | ||
|
||
[Fact(Skip = "Run manually")] | ||
public async Task Should_Receive_Associate_Response_For_Sms_Mfa_Enrollment() | ||
{ | ||
var request = | ||
new AssociateNewAuthenticatorRequest() | ||
{ | ||
Token = TestBaseUtils.GetVariable("AUTH0_AUTHENTICATOR_ENROLL_TOKEN"), | ||
ClientId = TestBaseUtils.GetVariable("AUTH0_CLIENT_ID"), | ||
ClientSecret = TestBaseUtils.GetVariable("AUTH0_CLIENT_SECRET"), | ||
AuthenticatorTypes = new List<string>() { "oob" }, | ||
OobChannels = new List<string>() { "sms" }, | ||
PhoneNumber = TestBaseUtils.GetVariable("MFA_PHONE_NUMBER") | ||
}; | ||
var response = await _authenticationApiClient.AssociateNewAuthenticatorAsync(request); | ||
response.Should().NotBeNull(); | ||
response.AuthenticatorType.Should().Be("oob"); | ||
response.BindingMethod.Should().Be("prompt"); | ||
response.OobChannel.Should().Be("sms"); | ||
|
||
response.OobCode.Should().NotBeNullOrEmpty().And.StartWith("Fe26."); | ||
} | ||
|
||
[Fact(Skip = "Run manually")] | ||
public async Task Should_Receive_MfaOobTokenResponse_For_Oob_Mfa_Verification() | ||
{ | ||
var request = new MfaOobTokenRequest() | ||
{ | ||
ClientId = TestBaseUtils.GetVariable("AUTH0_CLIENT_ID"), | ||
ClientSecret = TestBaseUtils.GetVariable("AUTH0_CLIENT_SECRET"), | ||
MfaToken = TestBaseUtils.GetVariable("MFA_TOKEN"), | ||
OobCode = TestBaseUtils.GetVariable("MFA_OOB_CODE"), | ||
BindingCode = TestBaseUtils.GetVariable("MFA_BINDING_CODE") | ||
}; | ||
|
||
var response = await _authenticationApiClient.GetTokenAsync(request); | ||
response.Should().NotBeNull(); | ||
response.AccessToken.Should().StartWith("ey"); | ||
response.ExpiresIn.Should().BeGreaterThan(0); | ||
response.TokenType.Should().Be("Bearer"); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is added through
ApplyClientAuthentication
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
made change