Skip to content
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

Adds support for CIBA #755

Merged
merged 1 commit into from
Dec 11, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Adds support for CIBA
kailash-b committed Dec 10, 2024
commit 93c6fa8347496348d837ed915b041959ed32a75f
54 changes: 54 additions & 0 deletions src/Auth0.AuthenticationApi/AuthenticationApiClient.cs
Original file line number Diff line number Diff line change
@@ -7,6 +7,7 @@
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using Auth0.AuthenticationApi.Models.Ciba;

namespace Auth0.AuthenticationApi
{
@@ -497,6 +498,59 @@ public Task<PushedAuthorizationRequestResponse> PushedAuthorizationRequestAsync(
);
}

/// <inheritdoc/>
public Task<ClientInitiatedBackchannelAuthorizationResponse> ClientInitiatedBackchannelAuthorization(ClientInitiatedBackchannelAuthorizationRequest request,
CancellationToken cancellationToken = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));

var body = new Dictionary<string, string> {
{ "client_id", request.ClientId },
{ "binding_message", request.BindingMessage },
{ "login_hint", request.LoginHint.ToString() }
};

body.AddIfNotEmpty("scope", request.Scope);
body.AddIfNotEmpty("audience", request.Audience);

body.AddAll(request.AdditionalProperties);

ApplyClientAuthentication(request, body, true);

return connection.SendAsync<ClientInitiatedBackchannelAuthorizationResponse>(
HttpMethod.Post,
BuildUri("/bc-authorize"),
body,
cancellationToken: cancellationToken
);
}

/// <inheritdoc/>
public async Task<ClientInitiatedBackchannelAuthorizationTokenResponse> GetTokenAsync(
ClientInitiatedBackchannelAuthorizationTokenRequest request,
CancellationToken cancellationToken = default)
{
if (request == null)
throw new ArgumentNullException(nameof(request));

var body = new Dictionary<string, string>
{
{"grant_type", "urn:openid:params:grant-type:ciba"},
{"auth_req_id", request.AuthRequestId},
{"client_id", request.ClientId}
};
ApplyClientAuthentication(request, body, true);
var response = await connection.SendAsync<ClientInitiatedBackchannelAuthorizationTokenResponse>(
HttpMethod.Post,
tokenUri,
body,
cancellationToken: cancellationToken
).ConfigureAwait(false);

return response;
}

/// <summary>
/// Disposes of any owned disposable resources such as a <see cref="IAuthenticationConnection"/>.
/// </summary>
Original file line number Diff line number Diff line change
@@ -87,12 +87,17 @@ private async Task<T> SendRequest<T>(HttpRequestMessage request, CancellationTok

var content = await response.Content.ReadAsStringAsync().ConfigureAwait(false);

return typeof(T) == typeof(string)
? (T)(object)content
: JsonConvert.DeserializeObject<T>(content, jsonSerializerSettings);
return DeserializeContent<T>(content);
}
}

internal T DeserializeContent<T>(string content)
{
return typeof(T) == typeof(string)
? (T)(object)content
: JsonConvert.DeserializeObject<T>(content, jsonSerializerSettings);
}

private void ApplyHeaders(HttpRequestMessage request, IDictionary<string, string> headers)
{
if (headers == null) return;
23 changes: 23 additions & 0 deletions src/Auth0.AuthenticationApi/IAuthenticationApiClient.cs
Original file line number Diff line number Diff line change
@@ -2,6 +2,7 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Auth0.AuthenticationApi.Models.Ciba;

namespace Auth0.AuthenticationApi
{
@@ -180,5 +181,27 @@ public interface IAuthenticationApiClient : IDisposable
/// a <see cref="PushedAuthorizationRequestResponse" /> with the details of the response.</returns>
Task<PushedAuthorizationRequestResponse> PushedAuthorizationRequestAsync(PushedAuthorizationRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Initiates a Client Initiated Backchannel Authorization flow.
/// </summary>
/// <param name="request"><see cref="ClientInitiatedBackchannelAuthorizationRequest"/></param>
/// <param name="cancellationToken"></param>
/// <returns><see cref="Task"/> representing the async operation containing
/// a <see cref="ClientInitiatedBackchannelAuthorizationResponse" /> with the details of the response.</returns>
Task<ClientInitiatedBackchannelAuthorizationResponse> ClientInitiatedBackchannelAuthorization(ClientInitiatedBackchannelAuthorizationRequest request,
CancellationToken cancellationToken = default);

/// <summary>
/// Requests an Access Token using the CIBA flow
/// </summary>
/// <param name="request"><see cref="ClientInitiatedBackchannelAuthorizationTokenRequest"/></param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
/// <returns><see cref="Task"/> representing the async operation containing
/// a <see cref="ClientInitiatedBackchannelAuthorizationTokenResponse" /> with the requested tokens.</returns>
/// <remarks>
/// This must be polled while the user is completing their part of the flow at an interval no more frequent than that returned by <see cref="ClientInitiatedBackchannelAuthorizationResponse"/>.
/// </remarks>
Task<ClientInitiatedBackchannelAuthorizationTokenResponse> GetTokenAsync(ClientInitiatedBackchannelAuthorizationTokenRequest request, CancellationToken cancellationToken = default);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System.Collections.Generic;
using Microsoft.IdentityModel.Tokens;

namespace Auth0.AuthenticationApi.Models.Ciba
{
/// <summary>
/// Contains information required for initiating a CIBA authorization request.
/// </summary>
public class ClientInitiatedBackchannelAuthorizationRequest : IClientAuthentication
{
/// <inheritdoc cref="IClientAuthentication.ClientId"/>
public string ClientId { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientSecret"/>
public string ClientSecret { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientAssertionSecurityKey"/>
public SecurityKey ClientAssertionSecurityKey { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientAssertionSecurityKeyAlgorithm"/>
public string ClientAssertionSecurityKeyAlgorithm { get; set; }

/// <summary>
/// A human-readable string intended to be displayed on both the device calling /bc-authorize and
/// the user’s authentication device (e.g. phone) to ensure the user is approving the correct request.
/// For example: “ABC-123-XYZ”.
/// </summary>
public string BindingMessage { get; set; }

/// <inheritdoc cref="Ciba.LoginHint"/>
public LoginHint LoginHint { get; set; }

/// <summary>
/// A space-separated list of OIDC and custom API scopes.
/// </summary>
public string Scope { get; set; }

/// <summary>
/// If you require an access token for an API, pass the unique identifier of the target API you want to access here
/// </summary>
public string Audience { get; set; }

/// <summary>
/// Used to configure a custom expiry time for this request.
/// </summary>
public int? RequestExpiry { get; set; }

/// <summary>
/// Any additional properties to use.
/// </summary>
public IDictionary<string, string> AdditionalProperties { get; set; } = new Dictionary<string, string>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
using Newtonsoft.Json;

namespace Auth0.AuthenticationApi.Models.Ciba
{
/// <summary>
/// Contains information about the client initiated backchannel authorization (CIBA) response.
/// </summary>
public class ClientInitiatedBackchannelAuthorizationResponse
{
/// <summary>
/// Unique id of the authorization request. Can be used further to poll for /oauth/token.
/// </summary>
[JsonProperty("auth_req_id")]
public string AuthRequestId { get; set; }

/// <summary>
/// Tells you how many seconds we have until the authentication request expires.
/// </summary>
[JsonProperty("expires_in")]
public int ExpiresIn { get; set; }

/// <summary>
/// Tells how many seconds you must leave between poll requests.
/// </summary>
[JsonProperty("interval")]
public int Interval { get; set; }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
using System.Collections.Generic;
using Microsoft.IdentityModel.Tokens;

namespace Auth0.AuthenticationApi.Models.Ciba
{
/// <summary>
/// Contains information required for request token using CIBA.
/// </summary>
public class ClientInitiatedBackchannelAuthorizationTokenRequest : IClientAuthentication
{
/// <inheritdoc cref="IClientAuthentication.ClientId"/>
public string ClientId { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientSecret"/>
public string ClientSecret { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientAssertionSecurityKey"/>
public SecurityKey ClientAssertionSecurityKey { get; set; }

/// <inheritdoc cref="IClientAuthentication.ClientAssertionSecurityKeyAlgorithm"/>
public string ClientAssertionSecurityKeyAlgorithm { get; set; }

/// <summary>
/// Unique identifier of the authorization request. This value will be returned from the call to /bc-authorize.
/// </summary>
public string AuthRequestId { get; set; }

/// <summary>
/// Any additional properties to use.
/// </summary>
public IDictionary<string, string> AdditionalProperties { get; set; } = new Dictionary<string, string>();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Newtonsoft.Json;

namespace Auth0.AuthenticationApi.Models.Ciba
{
public class ClientInitiatedBackchannelAuthorizationTokenResponse : AccessTokenResponse
{
[JsonProperty("scope")]
public string Scope { get; set; }
}
}
24 changes: 24 additions & 0 deletions src/Auth0.AuthenticationApi/Models/Ciba/LoginHint.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
using Newtonsoft.Json;

namespace Auth0.AuthenticationApi.Models.Ciba
{
/// <summary>
/// Contains information about the user to contact for authentication.
/// </summary>
public class LoginHint
{
[JsonProperty("format")]
public string Format { get; set; }

[JsonProperty("iss")]
public string Issuer { get; set; }

[JsonProperty("sub")]
public string Subject { get; set; }

public override string ToString()
{
return JsonConvert.SerializeObject(this);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
using System.Net;
using System.Net.Http;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using Moq.Protected;
using Newtonsoft.Json;
using FluentAssertions;
using Auth0.AuthenticationApi.Models.Ciba;
using Auth0.Core.Exceptions;
using Auth0.Tests.Shared;
using Xunit;

namespace Auth0.AuthenticationApi.IntegrationTests;

public class ClientInitiatedBackchannelAuthorizationTests : TestBase
{
[Fact]
public async void Ciba_request_should_succeed()
{
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var mockResponse = new ClientInitiatedBackchannelAuthorizationResponse()
{
AuthRequestId = "Random-Guid",
ExpiresIn = 300
};

var mockTokenResponse = new ClientInitiatedBackchannelAuthorizationTokenResponse()
{
AccessToken = "This is a mock access_token",
IdToken = "This is a mock ID token",
ExpiresIn = 300,
Scope = "openid"
};
var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL");

SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse));
SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse));

var httpClient = new HttpClient(mockHandler.Object);
var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient));

var response = await authenticationApiClient.ClientInitiatedBackchannelAuthorization(
new ClientInitiatedBackchannelAuthorizationRequest()
{
ClientId = GetVariable("AUTH0_CLIENT_ID"),
Scope = "openid profile",
ClientSecret = GetVariable("AUTH0_CLIENT_SECRET"),
BindingMessage = "BindingMessage",
LoginHint = new LoginHint()
{
Format = "iss_sub",
Issuer = "https://dx-sdks-testing.us.auth0.com/",
Subject = "auth0|6746de965777c7fc70547a11"
}
}
);

response.ExpiresIn.Should().Be(300);
response.AuthRequestId.Should().Be("Random-Guid");

var cibaTokenResponse = await authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = "Random-Guid",
ClientId = "ClientId",
ClientSecret = "ClientSecret"
}
);
cibaTokenResponse.Should().BeEquivalentTo(cibaTokenResponse);
mockHandler.VerifyAll();
}

[Fact]
public async void Ciba_request_in_pending_state()
{
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var mockResponse = new ClientInitiatedBackchannelAuthorizationResponse()
{
AuthRequestId = "Random-Guid",
ExpiresIn = 300
};

var mockTokenResponse =
"{\"error\": \"authorization_pending\",\n\"error_description\": \"The end-user authorization is pending\"\n}";

var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL");

SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse));
SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse), HttpStatusCode.BadRequest);

var httpClient = new HttpClient(mockHandler.Object);
var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient));

var response = await authenticationApiClient.ClientInitiatedBackchannelAuthorization(
new ClientInitiatedBackchannelAuthorizationRequest()
{
ClientId = GetVariable("AUTH0_CLIENT_ID"),
Scope = "openid profile",
ClientSecret = GetVariable("AUTH0_CLIENT_SECRET"),
BindingMessage = "BindingMessage",
LoginHint = new LoginHint()
{
Format = "iss_sub",
Issuer = "https://dx-sdks-testing.us.auth0.com/",
Subject = "auth0|6746de965777c7fc70547a11"
}
}
);

response.ExpiresIn.Should().Be(300);
response.AuthRequestId.Should().Be("Random-Guid");

var ex = await Assert.ThrowsAsync<ErrorApiException>(() => authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = "Random-Guid",
ClientId = "ClientId",
ClientSecret = "ClientSecret"
}
));
mockHandler.VerifyAll();
}

[Fact]
public async void Ciba_request_in_expired_or_rejected_state()
{
var mockHandler = new Mock<HttpMessageHandler>(MockBehavior.Strict);
var mockResponse = new ClientInitiatedBackchannelAuthorizationResponse()
{
AuthRequestId = "Random-Guid",
ExpiresIn = 300
};

var mockTokenResponse =
"{\n\"error\": \"access_denied\",\n\"error_description\": \"The end-user denied the authorization request or it\nhas been expired\"\n}";

var domain = GetVariable("AUTH0_AUTHENTICATION_API_URL");

SetupMockWith(mockHandler,$"https://{domain}/bc-authorize", JsonConvert.SerializeObject(mockResponse));
SetupMockWith(mockHandler,$"https://{domain}/oauth/token", JsonConvert.SerializeObject(mockTokenResponse), HttpStatusCode.BadRequest);

var httpClient = new HttpClient(mockHandler.Object);
var authenticationApiClient = new TestAuthenticationApiClient(domain, new TestHttpClientAuthenticationConnection(httpClient));

var response = await authenticationApiClient.ClientInitiatedBackchannelAuthorization(
new ClientInitiatedBackchannelAuthorizationRequest()
{
ClientId = GetVariable("AUTH0_CLIENT_ID"),
Scope = "openid profile",
ClientSecret = GetVariable("AUTH0_CLIENT_SECRET"),
BindingMessage = "BindingMessage",
LoginHint = new LoginHint()
{
Format = "iss_sub",
Issuer = "https://dx-sdks-testing.us.auth0.com/",
Subject = "auth0|6746de965777c7fc70547a11"
}
}
);

response.ExpiresIn.Should().Be(300);
response.AuthRequestId.Should().Be("Random-Guid");

var ex = await Assert.ThrowsAsync<ErrorApiException>(() => authenticationApiClient.GetTokenAsync(
new ClientInitiatedBackchannelAuthorizationTokenRequest()
{
AuthRequestId = "Random-Guid",
ClientId = "ClientId",
ClientSecret = "ClientSecret"
}
));
mockHandler.VerifyAll();
}

private static void SetupMockWith(Mock<HttpMessageHandler> mockHandler, string domain, string stringContent, HttpStatusCode code = HttpStatusCode.OK)
{
mockHandler.Protected()
.Setup<Task<HttpResponseMessage>>(
"SendAsync",
ItExpr.Is<HttpRequestMessage>(req => req.RequestUri.ToString() == domain),
ItExpr.IsAny<CancellationToken>()
)
.ReturnsAsync(new HttpResponseMessage()
{
StatusCode = code,
Content = new StringContent(stringContent, Encoding.UTF8, "application/json"),
});
}
}

Unchanged files with check annotations Beta

using System.Collections.Generic;
using Auth0.AuthenticationApi.Models;
using Auth0.ManagementApi.Models.Connections;
using Auth0.Tests.Shared;

Check warning on line 14 in tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs

GitHub Actions / build

The using directive for 'Auth0.Tests.Shared' appeared previously in this namespace

Check warning on line 14 in tests/Auth0.ManagementApi.IntegrationTests/ConnectionTests.cs

GitHub Actions / build

The using directive for 'Auth0.Tests.Shared' appeared previously in this namespace
using Newtonsoft.Json;
namespace Auth0.ManagementApi.IntegrationTests