Skip to content

Commit

Permalink
Added support for Back Channel Login (#682)
Browse files Browse the repository at this point in the history
  • Loading branch information
tanya732 authored Jan 2, 2025
1 parent 920affc commit 95b7e43
Show file tree
Hide file tree
Showing 7 changed files with 268 additions and 0 deletions.
55 changes: 55 additions & 0 deletions src/main/java/com/auth0/client/auth/AuthAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,61 @@ public AuthorizeUrlBuilder authorizeUrl(String redirectUri) {
return AuthorizeUrlBuilder.newInstance(baseUrl, clientId, redirectUri);
}

public Request<BackChannelAuthorizeResponse> authorizeBackChannel(String scope, String bindingMessage, Map<String, Object> loginHint) {
return authorizeBackChannel(scope, bindingMessage, loginHint, null, null);
}

public Request<BackChannelAuthorizeResponse> authorizeBackChannel(String scope, String bindingMessage, Map<String, Object> loginHint, String audience, Integer requestExpiry) {
Asserts.assertNotNull(scope, "scope");
Asserts.assertNotNull(bindingMessage, "binding message");
Asserts.assertNotNull(loginHint, "login hint");

String url = baseUrl
.newBuilder()
.addPathSegment("bc-authorize")
.build()
.toString();

FormBodyRequest<BackChannelAuthorizeResponse> request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference<BackChannelAuthorizeResponse>() {});

request.addParameter(KEY_CLIENT_ID, clientId);
addClientAuthentication(request, false);
request.addParameter("scope", scope);
request.addParameter("binding_message", bindingMessage);

if(Objects.nonNull(audience)){
request.addParameter(KEY_AUDIENCE, audience);
}
if(Objects.nonNull(requestExpiry)){
request.addParameter("request_expiry", requestExpiry);
}

try {
String loginHintJson = getMapper().writeValueAsString(loginHint);
request.addParameter("login_hint", loginHintJson);
}
catch (JsonProcessingException e) {
throw new IllegalArgumentException("'loginHint' must be a map that can be serialized to JSON", e);
}
return request;
}

public Request<BackChannelTokenResponse> getBackChannelLoginStatus(String authReqId, String grantType) {
Asserts.assertNotNull(authReqId, "auth req id");
Asserts.assertNotNull(grantType, "grant type");

String url = getTokenUrl();

FormBodyRequest<BackChannelTokenResponse> request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference<BackChannelTokenResponse>() {});

request.addParameter(KEY_CLIENT_ID, clientId);
addClientAuthentication(request, false);
request.addParameter("auth_req_id", authReqId);
request.addParameter(KEY_GRANT_TYPE, grantType);

return request;
}

/**
* Builds an authorization URL for Pushed Authorization Requests (PAR)
* @param requestUri the {@code request_uri} parameter from a successful pushed authorization request.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.auth0.json.auth;

import com.fasterxml.jackson.annotation.*;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BackChannelAuthorizeResponse {
@JsonProperty("auth_req_id")
private String authReqId;
@JsonProperty("expires_in")
private Long expiresIn;
@JsonProperty("interval")
private Integer interval;

@JsonCreator
public BackChannelAuthorizeResponse(@JsonProperty("auth_req_id") String authReqId, @JsonProperty("expires_in") Long expiresIn, @JsonProperty("interval") Integer interval) {
this.authReqId = authReqId;
this.expiresIn = expiresIn;
this.interval = interval;
}

/**
* Getter for the Auth Request ID.
* @return the Auth Request ID.
*/
public String getAuthReqId() {
return authReqId;
}

/**
* Getter for the Expires In value.
* @return the Expires In value.
*/
public Long getExpiresIn() {
return expiresIn;
}

/**
* Getter for the Interval value.
* @return the Interval value.
*/
public Integer getInterval() {
return interval;
}
}
34 changes: 34 additions & 0 deletions src/main/java/com/auth0/json/auth/BackChannelTokenResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.auth0.json.auth;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BackChannelTokenResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("id_token")
private String idToken;
@JsonProperty("expires_in")
private long expiresIn;
@JsonProperty("scope")
private String scope;

public String getAccessToken() {
return accessToken;
}

public String getIdToken() {
return idToken;
}

public long getExpiresIn() {
return expiresIn;
}

public String getScope() {
return scope;
}
}
2 changes: 2 additions & 0 deletions src/test/java/com/auth0/client/MockServer.java
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ public class MockServer {
public static final String PASSWORDLESS_EMAIL_RESPONSE = "src/test/resources/auth/passwordless_email.json";
public static final String PASSWORDLESS_SMS_RESPONSE = "src/test/resources/auth/passwordless_sms.json";
public static final String PUSHED_AUTHORIZATION_RESPONSE = "src/test/resources/auth/pushed_authorization_response.json";
public static final String BACK_CHANNEL_AUTHORIZE_RESPONSE = "src/test/resources/auth/back_channel_authorize_response.json";
public static final String BACK_CHANNEL_LOGIN_STATUS_RESPONSE = "src/test/resources/auth/back_channel_login_status_response.json";
public static final String AUTHENTICATOR_METHOD_BY_ID = "src/test/resources/mgmt/authenticator_method_by_id.json";
public static final String AUTHENTICATOR_METHOD_CREATE = "src/test/resources/mgmt/authenticator_method_create.json";
public static final String AUTHENTICATOR_METHOD_LIST = "src/test/resources/mgmt/authenticator_method_list.json";
Expand Down
121 changes: 121 additions & 0 deletions src/test/java/com/auth0/client/auth/AuthAPITest.java
Original file line number Diff line number Diff line change
Expand Up @@ -2009,6 +2009,127 @@ public void shouldThrowWhenCreatePushedAuthorizationJarRequestWithInvalidAuthDet
assertThat(e.getCause(), instanceOf(JsonProcessingException.class));
}

@Test
public void authorizeBackChannelWhenScopeIsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.authorizeBackChannel(null, "This is binding message", getLoginHint()),
"'scope' cannot be null!");
}

@Test
public void authorizeBackChannelWhenBindingMessageIsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.authorizeBackChannel("openid", null, getLoginHint()),
"'binding message' cannot be null!");
}

@Test
public void authorizeBackChannelWhenLoginHintIsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.authorizeBackChannel("openid", "This is binding message", null),
"'login hint' cannot be null!");
}

@Test
public void authorizeBackChannel() throws Exception {
Request<BackChannelAuthorizeResponse> request = api.authorizeBackChannel("openid", "This is binding message", getLoginHint());
assertThat(request, is(notNullValue()));

server.jsonResponse(BACK_CHANNEL_AUTHORIZE_RESPONSE, 200);
BackChannelAuthorizeResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/bc-authorize"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));

String body = URLDecoder.decode(readFromRequest(recordedRequest), StandardCharsets.UTF_8.name());
assertThat(body, containsString("scope=" + "openid"));
assertThat(body, containsString("client_id=" + CLIENT_ID));
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
assertThat(body, containsString("binding_message=This is binding message"));
assertThat(body, containsString("login_hint={\"sub\":\"auth0|user1\",\"format\":\"format1\",\"iss\":\"https://auth0.com\"}"));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthReqId(), not(emptyOrNullString()));
assertThat(response.getExpiresIn(), notNullValue());
assertThat(response.getInterval(), notNullValue());
}

@Test
public void authorizeBackChannelWithAudienceAndRequestExpiry() throws Exception {
Request<BackChannelAuthorizeResponse> request = api.authorizeBackChannel("openid", "This is binding message", getLoginHint(), "https://api.example.com", 300);
assertThat(request, is(notNullValue()));

server.jsonResponse(BACK_CHANNEL_AUTHORIZE_RESPONSE, 200);
BackChannelAuthorizeResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/bc-authorize"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));

String body = URLDecoder.decode(readFromRequest(recordedRequest), StandardCharsets.UTF_8.name());
assertThat(body, containsString("scope=" + "openid"));
assertThat(body, containsString("client_id=" + CLIENT_ID));
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
assertThat(body, containsString("binding_message=This is binding message"));
assertThat(body, containsString("login_hint={\"sub\":\"auth0|user1\",\"format\":\"format1\",\"iss\":\"https://auth0.com\"}"));
assertThat(body, containsString("request_expiry=" + 300));
assertThat(body, containsString("audience=" + "https://api.example.com"));

assertThat(response, is(notNullValue()));
assertThat(response.getAuthReqId(), not(emptyOrNullString()));
assertThat(response.getExpiresIn(), notNullValue());
assertThat(response.getInterval(), notNullValue());
}

private Map<String, Object> getLoginHint() {
Map<String, Object> loginHint = new HashMap<>();
loginHint.put("format", "format1");
loginHint.put("iss", "https://auth0.com");
loginHint.put("sub", "auth0|user1");
return loginHint;
}

@Test
public void getBackChannelLoginStatusWhenAuthReqIdIsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.getBackChannelLoginStatus(null, "ciba"),
"'auth req id' cannot be null!");
}

@Test
public void getBackChannelLoginStatusWhenGrantTypeIsNull() {
verifyThrows(IllegalArgumentException.class,
() -> api.getBackChannelLoginStatus("red_id_1", null),
"'grant type' cannot be null!");
}

@Test
public void getBackChannelLoginStatus() throws Exception {
Request<BackChannelTokenResponse> request = api.getBackChannelLoginStatus("red_id_1", "ciba");
assertThat(request, is(notNullValue()));

server.jsonResponse(BACK_CHANNEL_LOGIN_STATUS_RESPONSE, 200);
BackChannelTokenResponse response = request.execute().getBody();
RecordedRequest recordedRequest = server.takeRequest();

assertThat(recordedRequest, hasMethodAndPath(HttpMethod.POST, "/oauth/token"));
assertThat(recordedRequest, hasHeader("Content-Type", "application/x-www-form-urlencoded"));

String body = URLDecoder.decode(readFromRequest(recordedRequest), StandardCharsets.UTF_8.name());
assertThat(body, containsString("client_id=" + CLIENT_ID));
assertThat(body, containsString("client_secret=" + CLIENT_SECRET));
assertThat(body, containsString("auth_req_id=red_id_1"));
assertThat(body, containsString("grant_type=ciba"));

assertThat(response, is(notNullValue()));
assertThat(response.getAccessToken(), not(emptyOrNullString()));
assertThat(response.getIdToken(), not(emptyOrNullString()));
assertThat(response.getExpiresIn(), notNullValue());
assertThat(response.getScope(), not(emptyOrNullString()));
}


private Map<String, String> getQueryMap(String input) {
String[] params = input.split("&");

Expand Down
5 changes: 5 additions & 0 deletions src/test/resources/auth/back_channel_authorize_response.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"auth_req_id": "red_id_1",
"expires_in": 300,
"interval": 5
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"access_token": "eyJhbGciOiJkaXIi.....",
"id_token": "eyJhbGciOiJSUzI1NiIs.....",
"expires_in": 86400,
"scope": "openid"
}

0 comments on commit 95b7e43

Please sign in to comment.