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

Added support for Back Channel Login #682

Merged
merged 3 commits into from
Jan 2, 2025
Merged
Show file tree
Hide file tree
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
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
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"
}
Loading