diff --git a/docs/images/full-oauth-flow-oidc/1-initial-load.png b/docs/images/full-oauth-flow-oidc/1-initial-load.png index 6e90401..e2a2068 100644 Binary files a/docs/images/full-oauth-flow-oidc/1-initial-load.png and b/docs/images/full-oauth-flow-oidc/1-initial-load.png differ diff --git a/docs/images/full-oauth-flow-oidc/2-after-login-submit.png b/docs/images/full-oauth-flow-oidc/2-after-login-submit.png index 13dbc63..77ee1f2 100644 Binary files a/docs/images/full-oauth-flow-oidc/2-after-login-submit.png and b/docs/images/full-oauth-flow-oidc/2-after-login-submit.png differ diff --git a/docs/images/full-oauth-flow-oidc/3-after-consent-submit.png b/docs/images/full-oauth-flow-oidc/3-after-consent-submit.png index fbc61cd..a0fe66f 100644 Binary files a/docs/images/full-oauth-flow-oidc/3-after-consent-submit.png and b/docs/images/full-oauth-flow-oidc/3-after-consent-submit.png differ diff --git a/docs/images/remember-me/1-initial-load.png b/docs/images/remember-me/1-initial-load.png index 6e90401..e2a2068 100644 Binary files a/docs/images/remember-me/1-initial-load.png and b/docs/images/remember-me/1-initial-load.png differ diff --git a/docs/images/remember-me/2-after-login-submit.png b/docs/images/remember-me/2-after-login-submit.png index 13dbc63..77ee1f2 100644 Binary files a/docs/images/remember-me/2-after-login-submit.png and b/docs/images/remember-me/2-after-login-submit.png differ diff --git a/docs/images/remember-me/3-after-consent-submit.png b/docs/images/remember-me/3-after-consent-submit.png index dd8e5b8..2fff3af 100644 Binary files a/docs/images/remember-me/3-after-consent-submit.png and b/docs/images/remember-me/3-after-consent-submit.png differ diff --git a/docs/images/remember-me/4-initial-load-second-time.png b/docs/images/remember-me/4-initial-load-second-time.png index df99593..e2a2068 100644 Binary files a/docs/images/remember-me/4-initial-load-second-time.png and b/docs/images/remember-me/4-initial-load-second-time.png differ diff --git a/docs/images/remember-me/5-after-login-submit-second-time.png b/docs/images/remember-me/5-after-login-submit-second-time.png index a978c29..b3db897 100644 Binary files a/docs/images/remember-me/5-after-login-submit-second-time.png and b/docs/images/remember-me/5-after-login-submit-second-time.png differ diff --git a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentModelAndViewMapper.java b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentModelAndViewMapper.java index 5e18d21..dbac03f 100644 --- a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentModelAndViewMapper.java +++ b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentModelAndViewMapper.java @@ -26,6 +26,7 @@ private static ModelAndView handleSkip(Skip consentResponseSkip) { private static ModelAndView handleDisplayUI(DisplayUI displayUI) { return new ModelAndView("consent") .addObject("consentChallenge", displayUI.consentChallenge()) + .addObject("clientName", displayUI.clientName()) .addObject("scopes", displayUI.requestedScopes()); } diff --git a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentResponse.java b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentResponse.java index 61fd4f6..eff1cfb 100644 --- a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentResponse.java +++ b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentResponse.java @@ -10,7 +10,7 @@ public sealed interface ConsentResponse record Skip(String redirectTo) implements ConsentResponse {} - record DisplayUI(List requestedScopes, String consentChallenge) + record DisplayUI(String clientName, List requestedScopes, String consentChallenge) implements ConsentResponse {} record Accepted(String redirectTo) implements ConsentResponse {} diff --git a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentService.java b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentService.java index a9eded6..d446c67 100644 --- a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentService.java +++ b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/ConsentService.java @@ -13,6 +13,7 @@ import lombok.experimental.FieldDefaults; import lombok.val; import org.springframework.stereotype.Service; +import sh.ory.hydra.model.OAuth2ConsentRequest; @Service @RequiredArgsConstructor @@ -36,7 +37,23 @@ public ConsentResponse processInitialConsentRequest(@NonNull final String consen return new Skip(acceptConsentResponse.getRedirectTo()); } - return new DisplayUI(consentRequest.getRequestedScope(), consentChallenge); + return new DisplayUI( + clientName(consentRequest), + RequestedScope.fromNames(consentRequest.getRequestedScope()), + consentChallenge); + } + + // The consent challenge names the client asking for access. Prefer its display name, falling back + // to the client id, so the consent screen can tell the user who is requesting access. + private static String clientName(OAuth2ConsentRequest consentRequest) { + val client = consentRequest.getClient(); + if (client == null) { + return "An application"; + } + if (client.getClientName() != null && !client.getClientName().isBlank()) { + return client.getClientName(); + } + return client.getClientId(); } public ConsentResponse processConsentForm(@NonNull final ConsentForm consentForm) { diff --git a/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/RequestedScope.java b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/RequestedScope.java new file mode 100644 index 0000000..278af92 --- /dev/null +++ b/reference-app/src/main/java/com/ardetrick/oryhydrareference/consent/RequestedScope.java @@ -0,0 +1,24 @@ +package com.ardetrick.oryhydrareference.consent; + +import java.util.List; +import java.util.Map; + +/** + * A requested OAuth scope paired with a human-readable description. A real consent screen shows the + * user what they are granting ("Stay signed in") rather than the raw scope token + * ("offline_access"), so a consent provider keeps a catalog like this one. Scopes with no entry + * fall back to displaying just their raw name. + */ +public record RequestedScope(String name, String description) { + + private static final Map DESCRIPTIONS = + Map.of( + "openid", "Confirm your identity", + "profile", "Access your basic profile", + "email", "See your email address", + "offline_access", "Stay signed in by issuing a refresh token"); + + public static List fromNames(List names) { + return names.stream().map(name -> new RequestedScope(name, DESCRIPTIONS.get(name))).toList(); + } +} diff --git a/reference-app/src/main/resources/templates/consent.ftlh b/reference-app/src/main/resources/templates/consent.ftlh index f54e4c7..c8c7610 100644 --- a/reference-app/src/main/resources/templates/consent.ftlh +++ b/reference-app/src/main/resources/templates/consent.ftlh @@ -2,24 +2,36 @@ + Authorize access + -
- +
+

Authorize access

+

${clientName} wants to access your account. Choose what to allow.

+ + - - Requested Scopes
+
+ This will let it: + <#list scopes as scope> + + +
- <#list scopes as scope> - -
- + - -
- - - - +
+ + +
+ +
diff --git a/reference-app/src/test/java/com/ardetrick/oryhydrareference/OryHydraReferenceApplicationFunctionalTests.java b/reference-app/src/test/java/com/ardetrick/oryhydrareference/OryHydraReferenceApplicationFunctionalTests.java index c648f66..12d5bf1 100644 --- a/reference-app/src/test/java/com/ardetrick/oryhydrareference/OryHydraReferenceApplicationFunctionalTests.java +++ b/reference-app/src/test/java/com/ardetrick/oryhydrareference/OryHydraReferenceApplicationFunctionalTests.java @@ -147,7 +147,8 @@ public void registerTestClient() { .redirectUris(redirectUri) .grantTypes("authorization_code", "refresh_token") .responseTypes("code", "id_token") - .scope("offline_access", "openid", "offline", "profile")); + .scope("openid", "offline_access", "profile") + .put("client_name", "Demo Client")); } /** @@ -195,7 +196,7 @@ private URI getUriToInitiateFlow() { .addParameter("response_type", "code") .addParameter("client_id", clientId) .addParameter("redirect_uri", redirectUri) - .addParameter("scope", "offline_access openid offline profile") + .addParameter("scope", "openid offline_access profile") .addParameter("state", "12345678901234567890") .build(); } catch (URISyntaxException e) { @@ -601,7 +602,8 @@ public void quickStartFromLandingPageExchangesTokensInBrowser() { .redirectUris(appCallback) .grantTypes("authorization_code", "refresh_token") .responseTypes("code", "id_token") - .scope("offline_access", "openid", "offline", "profile")); + .scope("openid", "offline_access", "profile") + .put("client_name", "Demo Client")); val page = newPage(); diff --git a/reference-app/src/test/java/com/ardetrick/oryhydrareference/TestOryHydraReferenceApplication.java b/reference-app/src/test/java/com/ardetrick/oryhydrareference/TestOryHydraReferenceApplication.java index b7afc6a..338ef83 100644 --- a/reference-app/src/test/java/com/ardetrick/oryhydrareference/TestOryHydraReferenceApplication.java +++ b/reference-app/src/test/java/com/ardetrick/oryhydrareference/TestOryHydraReferenceApplication.java @@ -65,7 +65,7 @@ OryHydraContainer oryHydraContainer() { .redirectUris("http://localhost:8080/callback") .grantTypes("authorization_code", "refresh_token") .responseTypes("code", "id_token") - .scope("openid", "offline", "offline_access", "profile") + .scope("openid", "offline_access", "profile") .put("client_name", "Demo Client")) .build(); hydra.setPortBindings(List.of("4444:4444", "4445:4445"));