diff --git a/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs b/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs index 2d3c3297..af8c42e7 100644 --- a/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs +++ b/src/Auth0.AuthenticationApi/AuthenticationApiClient.cs @@ -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,33 @@ await AssertIdTokenValidIfExisting(response.IdToken, request.ClientId, request.S } /// + public async Task GetTokenAsync(MfaOobTokenRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + var body = new Dictionary() + { + { "grant_type", "http://auth0.com/oauth/grant-type/mfa-oob" }, + { "client_id", request.ClientId }, + { "mfa_token", request.MfaToken}, + { "oob_code", request.OobCode}, + }; + + body.AddIfNotEmpty("binding_code", request.BindingCode); + + ApplyClientAuthentication(request, body); + + return await connection.SendAsync( + HttpMethod.Post, + tokenUri, + body, + cancellationToken: cancellationToken); + } + /// public Task RevokeRefreshTokenAsync(RevokeRefreshTokenRequest request, CancellationToken cancellationToken = default) { if (request == null) @@ -494,6 +521,22 @@ public Task PushedAuthorizationRequestAsync( cancellationToken: cancellationToken ); } + + /// + public Task AssociateMfaAuthenticatorAsync(AssociateMfaAuthenticatorRequest request, CancellationToken cancellationToken = default) + { + if (request == null) + { + throw new ArgumentNullException(nameof(request)); + } + + return connection.SendAsync( + HttpMethod.Post, + BuildUri("mfa/associate"), + request, + BuildHeaders(request.Token), + cancellationToken); + } /// /// Disposes of any owned disposable resources such as a . diff --git a/src/Auth0.AuthenticationApi/IAuthenticationApiClient.cs b/src/Auth0.AuthenticationApi/IAuthenticationApiClient.cs index 268ad63e..6724c309 100644 --- a/src/Auth0.AuthenticationApi/IAuthenticationApiClient.cs +++ b/src/Auth0.AuthenticationApi/IAuthenticationApiClient.cs @@ -125,6 +125,15 @@ public interface IAuthenticationApiClient : IDisposable /// Task GetTokenAsync(DeviceCodeTokenRequest request, CancellationToken cancellationToken = default); + /// + /// Requests an Access Token using Oob MFA verification. + /// + /// containing request details to verify oob. + /// The cancellation token to cancel operation. + /// representing the async operation containing + /// a with the requested tokens. + Task GetTokenAsync(MfaOobTokenRequest request, CancellationToken cancellationToken = default); + /// /// Revokes refresh token provided in request. /// @@ -180,5 +189,19 @@ public interface IAuthenticationApiClient : IDisposable /// a with the details of the response. Task PushedAuthorizationRequestAsync(PushedAuthorizationRequest request, CancellationToken cancellationToken = default); + + /// + /// Sends a Mfa enrollment request + /// + /// containing information to enroll a new Authenticator. + /// The cancellation token to cancel operation. + /// representing the async operation containing + /// a with the details of the response. + /// + Task AssociateMfaAuthenticatorAsync( + AssociateMfaAuthenticatorRequest request, + CancellationToken cancellationToken = default); + + } } diff --git a/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorRequest.cs b/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorRequest.cs new file mode 100644 index 00000000..da2a96a0 --- /dev/null +++ b/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorRequest.cs @@ -0,0 +1,52 @@ +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; + +namespace Auth0.AuthenticationApi.Models +{ + public class AssociateMfaAuthenticatorRequest + { + [JsonIgnore] + public string Token { get; set; } + + /// Your application's Client ID. + [JsonProperty("client_id")] + public string ClientId { get; set; } + + /// + /// A JWT containing a signed assertion with your application credentials. Required when Private Key JWT is your application authentication + /// method. + /// + [JsonProperty("client_assertion")] + public string ClientAssertion { get; set; } + + /// + /// Your application's Client Secret. Required when the Token Endpoint Authentication Method field in your Application Settings is Post or + /// Basic. + /// + [JsonProperty("client_secret")] + public string ClientSecret { get; set; } + + /// + /// The value is urn:ietf:params:oauth:client-assertion-type:jwt-bearer. Required when Private Key JWT is the application authentication + /// method. + /// + [JsonProperty("client_assertion_type")] + public string ClientAssertionType { get; set; } + + /// The type of authenticators supported by the client. Value is an array with values "otp" or "oob". + [JsonProperty("authenticator_types")] + public List AuthenticatorTypes { get; set; } + + /// + /// The type of OOB channels supported by the client. An array with values "auth0", "sms", "voice". Required if authenticator_types include + /// oob. + /// + [JsonProperty("oob_channels")] + public List OobChannels { get; set; } + + /// The phone number to use for SMS or Voice. Required if oob_channels includes sms or voice. + [JsonProperty("phone_number")] + public string PhoneNumber { get; set; } + } +} diff --git a/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorResponse.cs b/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorResponse.cs new file mode 100644 index 00000000..9939261c --- /dev/null +++ b/src/Auth0.AuthenticationApi/Models/AssociateMfaAuthenticatorResponse.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using Newtonsoft.Json; + +namespace Auth0.AuthenticationApi.Models +{ + public class AssociateMfaAuthenticatorResponse + { + [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; } + + [JsonProperty("recovery_codes")] + public List RecoveryCodes { get; set; } + } +} diff --git a/src/Auth0.AuthenticationApi/Models/MfaOobTokenRequest.cs b/src/Auth0.AuthenticationApi/Models/MfaOobTokenRequest.cs new file mode 100644 index 00000000..e47aae40 --- /dev/null +++ b/src/Auth0.AuthenticationApi/Models/MfaOobTokenRequest.cs @@ -0,0 +1,44 @@ +using Microsoft.IdentityModel.Tokens; + +namespace Auth0.AuthenticationApi.Models +{ + public class MfaOobTokenRequest : IClientAuthentication + { + /// + /// Your application's Client ID. + /// + public string ClientId { get; set; } + + /// + /// Your application's Client Secret. + /// Required when the Token Endpoint Authentication Method field at your Application Settings is Post or Basic. + /// + public string ClientSecret { get; set; } + + /// + /// Security Key to use with Client Assertion + /// + public SecurityKey ClientAssertionSecurityKey { get; set; } + + /// + /// Algorithm for the Security Key to use with Client Assertion + /// + public string ClientAssertionSecurityKeyAlgorithm { get; set; } + + /// + /// The mfa_token you received from mfa_required error or access token with enroll scope and audience: https://{yourDomain}/mfa/ + /// + public string MfaToken { get; set; } + + /// + /// The oob code received from the challenge request. + /// + public string OobCode { get; set; } + + /// + /// 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. + /// + public string BindingCode { get; set; } + } +} diff --git a/src/Auth0.AuthenticationApi/Models/MfaOobTokenResponse.cs b/src/Auth0.AuthenticationApi/Models/MfaOobTokenResponse.cs new file mode 100644 index 00000000..c0dc9363 --- /dev/null +++ b/src/Auth0.AuthenticationApi/Models/MfaOobTokenResponse.cs @@ -0,0 +1,19 @@ +using Newtonsoft.Json; + +namespace Auth0.AuthenticationApi.Models +{ + public class MfaOobTokenResponse : TokenBase + { + /// + /// The value of the different scopes issued in the token + /// + [JsonProperty("scope")] + public string Scope { get; set; } + + /// + /// The lifetime (in seconds) of the token + /// + [JsonProperty("expires_in")] + public int ExpiresIn { get; set; } + } +} diff --git a/tests/Auth0.AuthenticationApi.IntegrationTests/MfaTests.cs b/tests/Auth0.AuthenticationApi.IntegrationTests/MfaTests.cs new file mode 100644 index 00000000..7c681a19 --- /dev/null +++ b/tests/Auth0.AuthenticationApi.IntegrationTests/MfaTests.cs @@ -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 AssociateMfaAuthenticatorRequest() + { + Token = TestBaseUtils.GetVariable("AUTH0_AUTHENTICATOR_ENROLL_TOKEN"), + ClientId = TestBaseUtils.GetVariable("AUTH0_CLIENT_ID"), + ClientSecret = TestBaseUtils.GetVariable("AUTH0_CLIENT_SECRET"), + AuthenticatorTypes = new List() { "oob" }, + OobChannels = new List() { "sms" }, + PhoneNumber = TestBaseUtils.GetVariable("MFA_PHONE_NUMBER") + }; + var response = await _authenticationApiClient.AssociateMfaAuthenticatorAsync(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"); + } + } +}