From fcdd41ea2aa640f56f5f9155fea30bd992fd4104 Mon Sep 17 00:00:00 2001 From: James Elliott Date: Tue, 8 Feb 2022 01:18:16 +1100 Subject: [PATCH] feat: oidc scope i18n (#2799) This adds i18n for the OIDC scope descriptsions descriptions. --- docs/configuration/identity-providers/oidc.md | 16 ++--- internal/oidc/client.go | 4 +- internal/oidc/client_test.go | 14 ++-- internal/oidc/const.go | 9 --- internal/oidc/helpers.go | 25 ------- internal/oidc/helpers_test.go | 49 -------------- internal/oidc/types.go | 20 ++---- web/src/i18n/locales/en.json | 9 ++- web/src/i18n/locales/es.json | 9 ++- web/src/services/Consent.ts | 14 +--- .../LoginPortal/ConsentView/ConsentView.tsx | 67 +++++++++++++++---- 11 files changed, 91 insertions(+), 145 deletions(-) delete mode 100644 internal/oidc/helpers.go delete mode 100644 internal/oidc/helpers_test.go diff --git a/docs/configuration/identity-providers/oidc.md b/docs/configuration/identity-providers/oidc.md index 21433739..a46b4f81 100644 --- a/docs/configuration/identity-providers/oidc.md +++ b/docs/configuration/identity-providers/oidc.md @@ -502,7 +502,7 @@ does. _**Important Note:** The claim `sub` is planned to be changed in the future to a randomly unique value to identify the individual user. Please use the claim `preferred_username` instead._ -| JWT Field | JWT Type | Authelia Attribute | Description | +| Claim | JWT Type | Authelia Attribute | Description | |:------------------:|:-------------:|:------------------:|:---------------------------------------------:| | sub | string | Username | The username the user used to login with | | scope | string | scopes | Granted scopes (space delimited) | @@ -521,15 +521,15 @@ individual user. Please use the claim `preferred_username` instead._ This scope includes the groups the authentication backend reports the user is a member of in the token. -| JWT Field | JWT Type | Authelia Attribute | Description | -|:---------:|:-------------:|:------------------:|:----------------------:| -| groups | array[string] | Groups | The users display name | +| Claim | JWT Type | Authelia Attribute | Description | +|:------:|:-------------:|:------------------:|:----------------------:| +| groups | array[string] | Groups | The users display name | ### email This scope includes the email information the authentication backend reports about the user in the token. -| JWT Field | JWT Type | Authelia Attribute | Description | +| Claim | JWT Type | Authelia Attribute | Description | |:--------------:|:-------------:|:------------------:|:---------------------------------------------------------:| | email | string | email[0] | The first email address in the list of emails | | email_verified | bool | _N/A_ | If the email is verified, assumed true for the time being | @@ -539,9 +539,9 @@ This scope includes the email information the authentication backend reports abo This scope includes the profile information the authentication backend reports about the user in the token. -| JWT Field | JWT Type | Authelia Attribute | Description | -|:---------:|:--------:|:------------------:|:----------------------:| -| name | string | display_name | The users display name | +| Claim | JWT Type | Authelia Attribute | Description | +|:-----:|:--------:|:------------------:|:----------------------:| +| name | string | display_name | The users display name | ## Endpoint Implementations diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 656e6524..4f5818bd 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -54,8 +54,8 @@ func (c InternalClient) GetConsentResponseBody(session *session.OIDCWorkflowSess } if session != nil { - body.Scopes = scopeNamesToScopes(session.RequestedScopes) - body.Audience = audienceNamesToAudience(session.RequestedAudience) + body.Scopes = session.RequestedScopes + body.Audience = session.RequestedAudience } return body diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index c1560919..52b2873f 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -73,8 +73,8 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) { consentRequestBody := c.GetConsentResponseBody(nil) assert.Equal(t, "", consentRequestBody.ClientID) assert.Equal(t, "", consentRequestBody.ClientDescription) - assert.Equal(t, []Scope(nil), consentRequestBody.Scopes) - assert.Equal(t, []Audience(nil), consentRequestBody.Audience) + assert.Equal(t, []string(nil), consentRequestBody.Scopes) + assert.Equal(t, []string(nil), consentRequestBody.Audience) c.ID = "myclient" c.Description = "My Client" @@ -83,13 +83,9 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) { RequestedAudience: []string{"https://example.com"}, RequestedScopes: []string{"openid", "groups"}, } - expectedScopes := []Scope{ - {"openid", "Use OpenID to verify your identity"}, - {"groups", "Access your group membership"}, - } - expectedAudiences := []Audience{ - {"https://example.com", "https://example.com"}, - } + + expectedScopes := []string{"openid", "groups"} + expectedAudiences := []string{"https://example.com"} consentRequestBody = c.GetConsentResponseBody(workflow) assert.Equal(t, "myclient", consentRequestBody.ClientID) diff --git a/internal/oidc/const.go b/internal/oidc/const.go index c9c782f6..6fae6957 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -1,14 +1,5 @@ package oidc -var scopeDescriptions = map[string]string{ - "openid": "Use OpenID to verify your identity", - "email": "Access your email addresses", - "profile": "Access your display name", - "groups": "Access your group membership", -} - -var audienceDescriptions = map[string]string{} - // Scope strings. const ( ScopeOpenID = "openid" diff --git a/internal/oidc/helpers.go b/internal/oidc/helpers.go deleted file mode 100644 index daa241ed..00000000 --- a/internal/oidc/helpers.go +++ /dev/null @@ -1,25 +0,0 @@ -package oidc - -func scopeNamesToScopes(scopeSlice []string) (scopes []Scope) { - for _, name := range scopeSlice { - if val, ok := scopeDescriptions[name]; ok { - scopes = append(scopes, Scope{name, val}) - } else { - scopes = append(scopes, Scope{name, name}) - } - } - - return scopes -} - -func audienceNamesToAudience(scopeSlice []string) (audience []Audience) { - for _, name := range scopeSlice { - if val, ok := audienceDescriptions[name]; ok { - audience = append(audience, Audience{name, val}) - } else { - audience = append(audience, Audience{name, name}) - } - } - - return audience -} diff --git a/internal/oidc/helpers_test.go b/internal/oidc/helpers_test.go deleted file mode 100644 index c4d79042..00000000 --- a/internal/oidc/helpers_test.go +++ /dev/null @@ -1,49 +0,0 @@ -package oidc - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestScopeNamesToScopes(t *testing.T) { - scopeNames := []string{"openid"} - - scopes := scopeNamesToScopes(scopeNames) - assert.Equal(t, "openid", scopes[0].Name) - assert.Equal(t, "Use OpenID to verify your identity", scopes[0].Description) - - scopeNames = []string{"groups"} - - scopes = scopeNamesToScopes(scopeNames) - assert.Equal(t, "groups", scopes[0].Name) - assert.Equal(t, "Access your group membership", scopes[0].Description) - - scopeNames = []string{"profile"} - - scopes = scopeNamesToScopes(scopeNames) - assert.Equal(t, "profile", scopes[0].Name) - assert.Equal(t, "Access your display name", scopes[0].Description) - - scopeNames = []string{"email"} - - scopes = scopeNamesToScopes(scopeNames) - assert.Equal(t, "email", scopes[0].Name) - assert.Equal(t, "Access your email addresses", scopes[0].Description) - - scopeNames = []string{"another"} - - scopes = scopeNamesToScopes(scopeNames) - assert.Equal(t, "another", scopes[0].Name) - assert.Equal(t, "another", scopes[0].Description) -} - -func TestAudienceNamesToScopes(t *testing.T) { - audienceNames := []string{"audience", "another_aud"} - - audiences := audienceNamesToAudience(audienceNames) - assert.Equal(t, "audience", audiences[0].Name) - assert.Equal(t, "audience", audiences[0].Description) - assert.Equal(t, "another_aud", audiences[1].Name) - assert.Equal(t, "another_aud", audiences[1].Description) -} diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 8fe84f45..140309ca 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -64,22 +64,10 @@ type AutheliaHasher struct{} // ConsentGetResponseBody schema of the response body of the consent GET endpoint. type ConsentGetResponseBody struct { - ClientID string `json:"client_id"` - ClientDescription string `json:"client_description"` - Scopes []Scope `json:"scopes"` - Audience []Audience `json:"audience"` -} - -// Scope represents the scope information. -type Scope struct { - Name string `json:"name"` - Description string `json:"description"` -} - -// Audience represents the audience information. -type Audience struct { - Name string `json:"name"` - Description string `json:"description"` + ClientID string `json:"client_id"` + ClientDescription string `json:"client_description"` + Scopes []string `json:"scopes"` + Audience []string `json:"audience"` } // WellKnownConfiguration is the OIDC well known config struct. diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 8750382d..305a6487 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -48,6 +48,13 @@ "Username": "Username", "You must open the link from the same device and browser that initiated the registration process": "You must open the link from the same device and browser that initiated the registration process", "You're being signed out and redirected": "You're being signed out and redirected", - "Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements." + "Your supplied password does not meet the password policy requirements": "Your supplied password does not meet the password policy requirements.", + "Use OpenID to verify your identity": "Use OpenID to verify your identity", + "Access your display name": "Access your display name", + "Access your group membership": "Access your group membership", + "Access your email addresses": "Access your email addresses", + "Accept": "Accept", + "Deny": "Deny", + "The above application is requesting the following permissions": "The above application is requesting the following permissions" } } diff --git a/web/src/i18n/locales/es.json b/web/src/i18n/locales/es.json index 30c23e7a..db35b7f1 100644 --- a/web/src/i18n/locales/es.json +++ b/web/src/i18n/locales/es.json @@ -48,6 +48,13 @@ "Username": "Usuario", "You must open the link from the same device and browser that initiated the registration process": "Debe abrir el link desde el mismo dispositivo y navegador desde el que inició el proceso de registración", "You're being signed out and redirected": "Cerrando Sesión y redirigiendo", - "Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas" + "Your supplied password does not meet the password policy requirements": "La contraseña suministrada no cumple con los requerimientos de la política de contraseñas", + "Use OpenID to verify your identity": "Utilizar OpenID para verificar su identidad", + "Access your display name": "Acceso a su nombre", + "Access your group membership": "Acceso a su(s) grupo(s)", + "Access your email addresses": "Acceso a su dirección de correo", + "Accept": "Aceptar", + "Deny": "Denegar", + "The above application is requesting the following permissions": "La aplicación solicita los siguientes permisos" } } diff --git a/web/src/services/Consent.ts b/web/src/services/Consent.ts index e3a9e35a..befecc1c 100644 --- a/web/src/services/Consent.ts +++ b/web/src/services/Consent.ts @@ -13,18 +13,8 @@ interface ConsentPostResponseBody { interface ConsentGetResponseBody { client_id: string; client_description: string; - scopes: Scope[]; - audience: Audience[]; -} - -interface Scope { - name: string; - description: string; -} - -interface Audience { - name: string; - description: string; + scopes: string[]; + audience: string[]; } export function getRequestedScopes() { diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx index c14bd57f..0b3fb63a 100644 --- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx +++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx @@ -1,7 +1,18 @@ import React, { useEffect, Fragment, ReactNode } from "react"; -import { Button, Grid, List, ListItem, ListItemIcon, ListItemText, Tooltip, makeStyles } from "@material-ui/core"; +import { + Button, + Grid, + List, + ListItem, + ListItemIcon, + ListItemText, + Tooltip, + Typography, + makeStyles, +} from "@material-ui/core"; import { AccountBox, CheckBox, Contacts, Drafts, Group } from "@material-ui/icons"; +import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router-dom"; import { FirstFactorRoute } from "@constants/Routes"; @@ -14,7 +25,7 @@ import LoadingPage from "@views/LoadingPage/LoadingPage"; export interface Props {} -function showListItemAvatar(id: string) { +function scopeNameToAvatar(id: string) { switch (id) { case "openid": return ; @@ -35,6 +46,7 @@ const ConsentView = function (props: Props) { const redirect = useRedirector(); const { createErrorNotification, resetNotification } = useNotifications(); const [resp, fetch, , err] = useRequestedScopes(); + const { t: translate } = useTranslation("Portal"); useEffect(() => { if (err) { @@ -47,6 +59,21 @@ const ConsentView = function (props: Props) { fetch(); }, [fetch]); + const translateScopeNameToDescription = (id: string): string => { + switch (id) { + case "openid": + return translate("Use OpenID to verify your identity"); + case "profile": + return translate("Access your display name"); + case "groups": + return translate("Access your group membership"); + case "email": + return translate("Access your email addresses"); + default: + return id; + } + }; + const handleAcceptConsent = async () => { // This case should not happen in theory because the buttons are disabled when response is undefined. if (!resp) { @@ -77,20 +104,31 @@ const ConsentView = function (props: Props) { -
- The application - {` ${resp?.client_description} (${resp?.client_id}) `} - is requesting the following permissions +
+ {resp !== undefined && resp.client_description !== "" ? ( + + + {resp.client_description} + + + ) : ( + + {resp?.client_id} + + )}
+ +
{translate("The above application is requesting the following permissions")}:
+
- {resp?.scopes.map((s) => ( - - - {showListItemAvatar(s.name)} - + {resp?.scopes.map((scope: string) => ( + + + {scopeNameToAvatar(scope)} + ))} @@ -108,7 +146,7 @@ const ConsentView = function (props: Props) { color="primary" variant="contained" > - Accept + {translate("Accept")} @@ -120,7 +158,7 @@ const ConsentView = function (props: Props) { color="secondary" variant="contained" > - Deny + {translate("Deny")} @@ -138,6 +176,9 @@ const useStyles = makeStyles((theme) => ({ display: "block", justifyContent: "center", }, + clientDescription: { + fontWeight: 600, + }, scopesListContainer: { textAlign: "center", },