From 641927387ccee9c081a7068cc9227b80cbe491f9 Mon Sep 17 00:00:00 2001 From: Dusan Jakub Date: Mon, 18 Sep 2023 16:25:54 +0200 Subject: [PATCH] test of Auth Code Grant flow --- .../com/ysoft/geecon/AuthCodeGrantTest.java | 66 +++-------- .../geecon/helpers/AuthorizationCodeFlow.java | 110 ++++++++++++++++++ .../ysoft/geecon/helpers/ConsentScreen.java | 27 +++++ .../com/ysoft/geecon/helpers/LoginScreen.java | 33 ++++++ 4 files changed, 186 insertions(+), 50 deletions(-) create mode 100644 src/test/java/com/ysoft/geecon/helpers/AuthorizationCodeFlow.java create mode 100644 src/test/java/com/ysoft/geecon/helpers/ConsentScreen.java create mode 100644 src/test/java/com/ysoft/geecon/helpers/LoginScreen.java diff --git a/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java b/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java index 0cec71e..4359912 100644 --- a/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java +++ b/src/test/java/com/ysoft/geecon/AuthCodeGrantTest.java @@ -2,34 +2,28 @@ package com.ysoft.geecon; import com.ysoft.geecon.dto.OAuthClient; import com.ysoft.geecon.dto.User; +import com.ysoft.geecon.helpers.AuthorizationCodeFlow; +import com.ysoft.geecon.helpers.ConsentScreen; +import com.ysoft.geecon.helpers.LoginScreen; import com.ysoft.geecon.repo.ClientsRepo; import com.ysoft.geecon.repo.UsersRepo; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import jakarta.inject.Inject; -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; -import org.jsoup.Connection; -import org.jsoup.Jsoup; import org.jsoup.nodes.Document; -import org.jsoup.nodes.FormElement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import java.io.IOException; -import java.net.URI; -import java.nio.charset.Charset; +import java.util.List; import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; -import static io.restassured.RestAssured.given; -import static io.restassured.http.ContentType.JSON; import static org.hamcrest.CoreMatchers.*; import static org.hamcrest.MatcherAssert.assertThat; @QuarkusTest public class AuthCodeGrantTest { + public static final OAuthClient CLIENT = new OAuthClient("myclient", "", null, "https://myserver:8888/success"); @Inject ClientsRepo clientsRepo; @Inject @@ -40,54 +34,26 @@ public class AuthCodeGrantTest { @BeforeEach void beforeAll() { - clientsRepo.register(new OAuthClient("myclient", "", null, "https://myserver:8888/success")); + clientsRepo.register(CLIENT); usersRepo.register(new User("bob", "password")); } @Test public void authCodeGrant() throws IOException { - String state = "test state is not random"; - FormElement login = Jsoup.connect(authUrl) - .data("client_id", "myclient") - .data("redirect_uri", "https://myserver:8888/success") - .data("state", state) - .data("scope", "scope1 scope2") - .get().forms().get(0); - login.getElementsByAttributeValue("name", "username").val("bob"); - login.getElementsByAttributeValue("name", "password").val("password"); + AuthorizationCodeFlow flow = new AuthorizationCodeFlow(authUrl, CLIENT); + LoginScreen loginScreen = flow.start(Map.of("scope", "scope1 scope2")); - Document consentsDoc = login.submit().post(); - FormElement consents = consentsDoc.expectForm("form"); + ConsentScreen consentScreen = loginScreen.submitCorrect("bob", "password"); + assertThat(consentScreen.getScopes(), is(List.of("scope1", "scope2"))); - consents.expectFirst("input[name=scope][value=scope1]"); - consents.expectFirst("input[name=scope][value=scope2]"); + Document submit = consentScreen.submit(); + flow.parseAndValidateRedirect(submit.connection().response()); - Document success = consents.submit().followRedirects(false).post(); - Connection.Response response = success.connection().response(); - assertThat(response.statusCode(), is(303)); - assertThat(response.header("location"), startsWith("https://myserver:8888/success")); + assertThat(flow.getCode(), is(notNullValue())); + assertThat(flow.getToken(), is(nullValue())); + flow.exchangeCode(); - URI location = URI.create(Objects.requireNonNull(response.header("location"))); - Map query = URLEncodedUtils.parse(location.getQuery(), Charset.defaultCharset()) - .stream().collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); - - assertThat(query.get("state"), is(state)); - assertThat(query.get("code"), is(notNullValue())); - - given() - .formParam("grant_type", "authorization_code") - .formParam("client_id", "myclient") - .formParam("redirect_uri", "https://myserver:8888/success") - .formParam("code", query.get("code")) - .when() - .post("/auth/token") - .then() - .statusCode(200) - .contentType(JSON) - .body("token_type", is("Bearer")) - .body("expires_in", is(notNullValue())) - .body("access_token", is(notNullValue())) - .body("refresh_token", is(notNullValue())); + assertThat(flow.getToken(), is(notNullValue())); } } \ No newline at end of file diff --git a/src/test/java/com/ysoft/geecon/helpers/AuthorizationCodeFlow.java b/src/test/java/com/ysoft/geecon/helpers/AuthorizationCodeFlow.java new file mode 100644 index 0000000..1baf6c5 --- /dev/null +++ b/src/test/java/com/ysoft/geecon/helpers/AuthorizationCodeFlow.java @@ -0,0 +1,110 @@ +package com.ysoft.geecon.helpers; + +import com.ysoft.geecon.dto.AccessTokenResponse; +import com.ysoft.geecon.dto.OAuthClient; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.jsoup.Connection; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +import java.io.IOException; +import java.net.URI; +import java.nio.charset.Charset; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +import static io.restassured.RestAssured.given; +import static io.restassured.http.ContentType.JSON; +import static org.hamcrest.CoreMatchers.*; +import static org.hamcrest.MatcherAssert.assertThat; + +public class AuthorizationCodeFlow { + private final String authUrl; + private final OAuthClient client; + private String state = "testStateIsNotRandom"; + private String code; + private String token; + private String idToken; + + public AuthorizationCodeFlow(String authUrl, OAuthClient client) { + this.authUrl = authUrl; + this.client = client; + } + + public LoginScreen start(Map additionalData) throws IOException { + var data = defaultQuery(); + if (additionalData != null) { + data.putAll(additionalData); + } + + Document login = Jsoup.connect(authUrl) + .data(data) + .get(); + + return new LoginScreen(login); + } + + private Map defaultQuery() { + var map = new HashMap(); + map.put("client_id", client.clientId()); + map.put("redirect_uri", client.redirectUri()); + map.put("state", state); + return map; + } + + public void parseAndValidateRedirect(Connection.Response response) { + assertThat(response.statusCode(), is(303)); + assertThat(response.header("location"), startsWith(client.redirectUri())); + + URI location = URI.create(Objects.requireNonNull(response.header("location"))); + Map query = URLEncodedUtils.parse(location.getQuery(), Charset.defaultCharset()) + .stream().collect(Collectors.toMap(NameValuePair::getName, NameValuePair::getValue)); + + assertThat(query.get("state"), is(state)); + assertThat(query.get("code"), is(notNullValue())); + + code = query.get("code"); + token = query.get("token"); + idToken = query.get("id_token"); + } + + public AccessTokenResponse exchangeCode() { + AccessTokenResponse accessTokenResponse = given() + .formParam("grant_type", "authorization_code") + .formParam("client_id", client.clientId()) + .formParam("redirect_uri", client.redirectUri()) + .formParam("code", code) + .when() + .post("/auth/token") + .then() + .statusCode(200) + .contentType(JSON) + .body("token_type", is("Bearer")) + .body("expires_in", is(notNullValue())) + .body("access_token", is(notNullValue())) + .body("refresh_token", is(notNullValue())) + .extract().body().as(AccessTokenResponse.class); + token = accessTokenResponse.accessToken(); + idToken = accessTokenResponse.idToken(); + return accessTokenResponse; + } + + public String getState() { + return state; + } + + public String getCode() { + return code; + } + + public String getToken() { + return token; + } + + public String getIdToken() { + return idToken; + } +} diff --git a/src/test/java/com/ysoft/geecon/helpers/ConsentScreen.java b/src/test/java/com/ysoft/geecon/helpers/ConsentScreen.java new file mode 100644 index 0000000..272b769 --- /dev/null +++ b/src/test/java/com/ysoft/geecon/helpers/ConsentScreen.java @@ -0,0 +1,27 @@ +package com.ysoft.geecon.helpers; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.nodes.FormElement; +import org.jsoup.select.Elements; + +import java.io.IOException; +import java.util.List; + +public class ConsentScreen { + + private final FormElement consents; + + public ConsentScreen(Document document) { + consents = document.expectForm("form"); + } + + public List getScopes() { + Elements checkboxes = consents.select("input[name=scope]"); + return checkboxes.stream().map(Element::val).toList(); + } + + public Document submit() throws IOException { + return consents.submit().followRedirects(false).post(); + } +} diff --git a/src/test/java/com/ysoft/geecon/helpers/LoginScreen.java b/src/test/java/com/ysoft/geecon/helpers/LoginScreen.java new file mode 100644 index 0000000..a05dbac --- /dev/null +++ b/src/test/java/com/ysoft/geecon/helpers/LoginScreen.java @@ -0,0 +1,33 @@ +package com.ysoft.geecon.helpers; + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.FormElement; + +import java.io.IOException; + +public class LoginScreen { + + private final FormElement form; + + public LoginScreen(Document doc) { + this.form = doc.expectForm("form"); + ; + } + + public Document submit(String username, String password) throws IOException { + form.getElementsByAttributeValue("name", "username").val(username); + form.getElementsByAttributeValue("name", "password").val(password); + + return form.submit().post(); + } + + public ConsentScreen submitCorrect(String username, String password) throws IOException { + Document posted = submit(username, password); + return new ConsentScreen(posted); + } + + public LoginScreen submitWrong(String username, String password) throws IOException { + Document posted = submit(username, password); + return new LoginScreen(posted); + } +}