From 9b917b18cab27eaef5d6345a36d3bb1dcdfd180f Mon Sep 17 00:00:00 2001 From: tanya732 Date: Tue, 3 Dec 2024 15:11:53 +0530 Subject: [PATCH 1/2] Added support for back channel login Auth API's --- .../java/com/auth0/client/auth/AuthAPI.java | 57 +++++++++++++++++++ .../auth/BackChannelAuthorizeResponse.java | 45 +++++++++++++++ .../json/auth/BackChannelTokenResponse.java | 34 +++++++++++ 3 files changed, 136 insertions(+) create mode 100644 src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java create mode 100644 src/main/java/com/auth0/json/auth/BackChannelTokenResponse.java diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index ad353a32..3f8a16ce 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -225,6 +225,63 @@ public AuthorizeUrlBuilder authorizeUrl(String redirectUri) { return AuthorizeUrlBuilder.newInstance(baseUrl, clientId, redirectUri); } + public Request backChannelAuthorize(String scope, String bindingMessage, Map loginHint) { + return backChannelAuthorize(scope, bindingMessage, loginHint, null, null); + } + + public Request backChannelAuthorize(String scope, String bindingMessage, Map 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 request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference() {}); + + request.addParameter(KEY_CLIENT_ID, clientId); + if(Objects.nonNull(clientSecret)){ + request.addParameter(KEY_CLIENT_SECRET, clientSecret); + } + 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 getBackChannelLoginStatus(String authReqId, String grantType) { + Asserts.assertNotNull(authReqId, "authReqId"); + Asserts.assertNotNull(grantType, "grantType"); + + String url = getTokenUrl(); + + FormBodyRequest request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference() {}); + + 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. diff --git a/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java b/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java new file mode 100644 index 00000000..c5cedca8 --- /dev/null +++ b/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java @@ -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 int interval; + + @JsonCreator + public BackChannelAuthorizeResponse(@JsonProperty("auth_req_id") String authReqId, @JsonProperty("expires_in") int expiresIn, @JsonProperty("interval") int 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 int getInterval() { + return interval; + } +} diff --git a/src/main/java/com/auth0/json/auth/BackChannelTokenResponse.java b/src/main/java/com/auth0/json/auth/BackChannelTokenResponse.java new file mode 100644 index 00000000..ab632f98 --- /dev/null +++ b/src/main/java/com/auth0/json/auth/BackChannelTokenResponse.java @@ -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; + } +} From 8953c0847af4453cc67583c294c70be9aa0cb36f Mon Sep 17 00:00:00 2001 From: tanya732 Date: Thu, 12 Dec 2024 13:41:15 +0530 Subject: [PATCH 2/2] added testcases --- .../java/com/auth0/client/auth/AuthAPI.java | 14 +- .../auth/BackChannelAuthorizeResponse.java | 10 +- .../java/com/auth0/client/MockServer.java | 2 + .../com/auth0/client/auth/AuthAPITest.java | 121 ++++++++++++++++++ .../auth/back_channel_authorize_response.json | 5 + .../back_channel_login_status_response.json | 6 + 6 files changed, 145 insertions(+), 13 deletions(-) create mode 100644 src/test/resources/auth/back_channel_authorize_response.json create mode 100644 src/test/resources/auth/back_channel_login_status_response.json diff --git a/src/main/java/com/auth0/client/auth/AuthAPI.java b/src/main/java/com/auth0/client/auth/AuthAPI.java index 3f8a16ce..4334223c 100644 --- a/src/main/java/com/auth0/client/auth/AuthAPI.java +++ b/src/main/java/com/auth0/client/auth/AuthAPI.java @@ -225,11 +225,11 @@ public AuthorizeUrlBuilder authorizeUrl(String redirectUri) { return AuthorizeUrlBuilder.newInstance(baseUrl, clientId, redirectUri); } - public Request backChannelAuthorize(String scope, String bindingMessage, Map loginHint) { - return backChannelAuthorize(scope, bindingMessage, loginHint, null, null); + public Request authorizeBackChannel(String scope, String bindingMessage, Map loginHint) { + return authorizeBackChannel(scope, bindingMessage, loginHint, null, null); } - public Request backChannelAuthorize(String scope, String bindingMessage, Map loginHint, String audience, Integer requestExpiry) { + public Request authorizeBackChannel(String scope, String bindingMessage, Map loginHint, String audience, Integer requestExpiry) { Asserts.assertNotNull(scope, "scope"); Asserts.assertNotNull(bindingMessage, "binding message"); Asserts.assertNotNull(loginHint, "login hint"); @@ -243,9 +243,7 @@ public Request backChannelAuthorize(String scope, FormBodyRequest request = new FormBodyRequest<>(client, null, url, HttpMethod.POST, new TypeReference() {}); request.addParameter(KEY_CLIENT_ID, clientId); - if(Objects.nonNull(clientSecret)){ - request.addParameter(KEY_CLIENT_SECRET, clientSecret); - } + addClientAuthentication(request, false); request.addParameter("scope", scope); request.addParameter("binding_message", bindingMessage); @@ -267,8 +265,8 @@ public Request backChannelAuthorize(String scope, } public Request getBackChannelLoginStatus(String authReqId, String grantType) { - Asserts.assertNotNull(authReqId, "authReqId"); - Asserts.assertNotNull(grantType, "grantType"); + Asserts.assertNotNull(authReqId, "auth req id"); + Asserts.assertNotNull(grantType, "grant type"); String url = getTokenUrl(); diff --git a/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java b/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java index c5cedca8..1765867d 100644 --- a/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java +++ b/src/main/java/com/auth0/json/auth/BackChannelAuthorizeResponse.java @@ -8,12 +8,12 @@ public class BackChannelAuthorizeResponse { @JsonProperty("auth_req_id") private String authReqId; @JsonProperty("expires_in") - private long expiresIn; + private Long expiresIn; @JsonProperty("interval") - private int interval; + private Integer interval; @JsonCreator - public BackChannelAuthorizeResponse(@JsonProperty("auth_req_id") String authReqId, @JsonProperty("expires_in") int expiresIn, @JsonProperty("interval") int interval) { + 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; @@ -31,7 +31,7 @@ public String getAuthReqId() { * Getter for the Expires In value. * @return the Expires In value. */ - public long getExpiresIn() { + public Long getExpiresIn() { return expiresIn; } @@ -39,7 +39,7 @@ public long getExpiresIn() { * Getter for the Interval value. * @return the Interval value. */ - public int getInterval() { + public Integer getInterval() { return interval; } } diff --git a/src/test/java/com/auth0/client/MockServer.java b/src/test/java/com/auth0/client/MockServer.java index cc65cab6..54bfc049 100644 --- a/src/test/java/com/auth0/client/MockServer.java +++ b/src/test/java/com/auth0/client/MockServer.java @@ -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"; diff --git a/src/test/java/com/auth0/client/auth/AuthAPITest.java b/src/test/java/com/auth0/client/auth/AuthAPITest.java index 535fec03..4e8366a4 100644 --- a/src/test/java/com/auth0/client/auth/AuthAPITest.java +++ b/src/test/java/com/auth0/client/auth/AuthAPITest.java @@ -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 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 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 getLoginHint() { + Map 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 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 getQueryMap(String input) { String[] params = input.split("&"); diff --git a/src/test/resources/auth/back_channel_authorize_response.json b/src/test/resources/auth/back_channel_authorize_response.json new file mode 100644 index 00000000..bc0649f0 --- /dev/null +++ b/src/test/resources/auth/back_channel_authorize_response.json @@ -0,0 +1,5 @@ +{ + "auth_req_id": "red_id_1", + "expires_in": 300, + "interval": 5 +} diff --git a/src/test/resources/auth/back_channel_login_status_response.json b/src/test/resources/auth/back_channel_login_status_response.json new file mode 100644 index 00000000..bc1676e2 --- /dev/null +++ b/src/test/resources/auth/back_channel_login_status_response.json @@ -0,0 +1,6 @@ +{ + "access_token": "eyJhbGciOiJkaXIi.....", + "id_token": "eyJhbGciOiJSUzI1NiIs.....", + "expires_in": 86400, + "scope": "openid" +}