feat: oidc scope i18n (#2799)

This adds i18n for the OIDC scope descriptsions descriptions.
pull/2849/head^2
James Elliott 2022-02-08 01:18:16 +11:00 committed by GitHub
parent 26236f491e
commit fcdd41ea2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 91 additions and 145 deletions

View File

@ -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 _**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._ 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 | | sub | string | Username | The username the user used to login with |
| scope | string | scopes | Granted scopes (space delimited) | | 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. 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 | | Claim | JWT Type | Authelia Attribute | Description |
|:---------:|:-------------:|:------------------:|:----------------------:| |:------:|:-------------:|:------------------:|:----------------------:|
| groups | array[string] | Groups | The users display name | | groups | array[string] | Groups | The users display name |
### email ### email
This scope includes the email information the authentication backend reports about the user in the token. 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 | 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 | | email_verified | bool | _N/A_ | If the email is verified, assumed true for the time being |
@ -539,8 +539,8 @@ 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. This scope includes the profile information the authentication backend reports about the user in the token.
| JWT Field | JWT Type | Authelia Attribute | Description | | Claim | JWT Type | Authelia Attribute | Description |
|:---------:|:--------:|:------------------:|:----------------------:| |:-----:|:--------:|:------------------:|:----------------------:|
| name | string | display_name | The users display name | | name | string | display_name | The users display name |
## Endpoint Implementations ## Endpoint Implementations

View File

@ -54,8 +54,8 @@ func (c InternalClient) GetConsentResponseBody(session *session.OIDCWorkflowSess
} }
if session != nil { if session != nil {
body.Scopes = scopeNamesToScopes(session.RequestedScopes) body.Scopes = session.RequestedScopes
body.Audience = audienceNamesToAudience(session.RequestedAudience) body.Audience = session.RequestedAudience
} }
return body return body

View File

@ -73,8 +73,8 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
consentRequestBody := c.GetConsentResponseBody(nil) consentRequestBody := c.GetConsentResponseBody(nil)
assert.Equal(t, "", consentRequestBody.ClientID) assert.Equal(t, "", consentRequestBody.ClientID)
assert.Equal(t, "", consentRequestBody.ClientDescription) assert.Equal(t, "", consentRequestBody.ClientDescription)
assert.Equal(t, []Scope(nil), consentRequestBody.Scopes) assert.Equal(t, []string(nil), consentRequestBody.Scopes)
assert.Equal(t, []Audience(nil), consentRequestBody.Audience) assert.Equal(t, []string(nil), consentRequestBody.Audience)
c.ID = "myclient" c.ID = "myclient"
c.Description = "My Client" c.Description = "My Client"
@ -83,13 +83,9 @@ func TestInternalClient_GetConsentResponseBody(t *testing.T) {
RequestedAudience: []string{"https://example.com"}, RequestedAudience: []string{"https://example.com"},
RequestedScopes: []string{"openid", "groups"}, RequestedScopes: []string{"openid", "groups"},
} }
expectedScopes := []Scope{
{"openid", "Use OpenID to verify your identity"}, expectedScopes := []string{"openid", "groups"}
{"groups", "Access your group membership"}, expectedAudiences := []string{"https://example.com"}
}
expectedAudiences := []Audience{
{"https://example.com", "https://example.com"},
}
consentRequestBody = c.GetConsentResponseBody(workflow) consentRequestBody = c.GetConsentResponseBody(workflow)
assert.Equal(t, "myclient", consentRequestBody.ClientID) assert.Equal(t, "myclient", consentRequestBody.ClientID)

View File

@ -1,14 +1,5 @@
package oidc 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. // Scope strings.
const ( const (
ScopeOpenID = "openid" ScopeOpenID = "openid"

View File

@ -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
}

View File

@ -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)
}

View File

@ -66,20 +66,8 @@ type AutheliaHasher struct{}
type ConsentGetResponseBody struct { type ConsentGetResponseBody struct {
ClientID string `json:"client_id"` ClientID string `json:"client_id"`
ClientDescription string `json:"client_description"` ClientDescription string `json:"client_description"`
Scopes []Scope `json:"scopes"` Scopes []string `json:"scopes"`
Audience []Audience `json:"audience"` Audience []string `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"`
} }
// WellKnownConfiguration is the OIDC well known config struct. // WellKnownConfiguration is the OIDC well known config struct.

View File

@ -48,6 +48,13 @@
"Username": "Username", "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 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", "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"
} }
} }

View File

@ -48,6 +48,13 @@
"Username": "Usuario", "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 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", "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"
} }
} }

View File

@ -13,18 +13,8 @@ interface ConsentPostResponseBody {
interface ConsentGetResponseBody { interface ConsentGetResponseBody {
client_id: string; client_id: string;
client_description: string; client_description: string;
scopes: Scope[]; scopes: string[];
audience: Audience[]; audience: string[];
}
interface Scope {
name: string;
description: string;
}
interface Audience {
name: string;
description: string;
} }
export function getRequestedScopes() { export function getRequestedScopes() {

View File

@ -1,7 +1,18 @@
import React, { useEffect, Fragment, ReactNode } from "react"; 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 { AccountBox, CheckBox, Contacts, Drafts, Group } from "@material-ui/icons";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { FirstFactorRoute } from "@constants/Routes"; import { FirstFactorRoute } from "@constants/Routes";
@ -14,7 +25,7 @@ import LoadingPage from "@views/LoadingPage/LoadingPage";
export interface Props {} export interface Props {}
function showListItemAvatar(id: string) { function scopeNameToAvatar(id: string) {
switch (id) { switch (id) {
case "openid": case "openid":
return <AccountBox />; return <AccountBox />;
@ -35,6 +46,7 @@ const ConsentView = function (props: Props) {
const redirect = useRedirector(); const redirect = useRedirector();
const { createErrorNotification, resetNotification } = useNotifications(); const { createErrorNotification, resetNotification } = useNotifications();
const [resp, fetch, , err] = useRequestedScopes(); const [resp, fetch, , err] = useRequestedScopes();
const { t: translate } = useTranslation("Portal");
useEffect(() => { useEffect(() => {
if (err) { if (err) {
@ -47,6 +59,21 @@ const ConsentView = function (props: Props) {
fetch(); fetch();
}, [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 () => { const handleAcceptConsent = async () => {
// This case should not happen in theory because the buttons are disabled when response is undefined. // This case should not happen in theory because the buttons are disabled when response is undefined.
if (!resp) { if (!resp) {
@ -77,20 +104,31 @@ const ConsentView = function (props: Props) {
<LoginLayout id="consent-stage" title={`Permissions Request`} showBrand> <LoginLayout id="consent-stage" title={`Permissions Request`} showBrand>
<Grid container> <Grid container>
<Grid item xs={12}> <Grid item xs={12}>
<div style={{ textAlign: "left" }}> <div>
The application {resp !== undefined && resp.client_description !== "" ? (
<b>{` ${resp?.client_description} (${resp?.client_id}) `}</b> <Tooltip title={"Client ID: " + resp.client_id}>
is requesting the following permissions <Typography className={classes.clientDescription}>
{resp.client_description}
</Typography>
</Tooltip>
) : (
<Tooltip title={"Client ID: " + resp?.client_id}>
<Typography className={classes.clientDescription}>{resp?.client_id}</Typography>
</Tooltip>
)}
</div> </div>
</Grid> </Grid>
<Grid item xs={12}>
<div>{translate("The above application is requesting the following permissions")}:</div>
</Grid>
<Grid item xs={12}> <Grid item xs={12}>
<div className={classes.scopesListContainer}> <div className={classes.scopesListContainer}>
<List className={classes.scopesList}> <List className={classes.scopesList}>
{resp?.scopes.map((s) => ( {resp?.scopes.map((scope: string) => (
<Tooltip title={"Scope " + s.name}> <Tooltip title={"Scope " + scope}>
<ListItem id={"scope-" + s.name} dense> <ListItem id={"scope-" + scope} dense>
<ListItemIcon>{showListItemAvatar(s.name)}</ListItemIcon> <ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
<ListItemText primary={s.description} /> <ListItemText primary={translateScopeNameToDescription(scope)} />
</ListItem> </ListItem>
</Tooltip> </Tooltip>
))} ))}
@ -108,7 +146,7 @@ const ConsentView = function (props: Props) {
color="primary" color="primary"
variant="contained" variant="contained"
> >
Accept {translate("Accept")}
</Button> </Button>
</Grid> </Grid>
<Grid item xs={6}> <Grid item xs={6}>
@ -120,7 +158,7 @@ const ConsentView = function (props: Props) {
color="secondary" color="secondary"
variant="contained" variant="contained"
> >
Deny {translate("Deny")}
</Button> </Button>
</Grid> </Grid>
</Grid> </Grid>
@ -138,6 +176,9 @@ const useStyles = makeStyles((theme) => ({
display: "block", display: "block",
justifyContent: "center", justifyContent: "center",
}, },
clientDescription: {
fontWeight: 600,
},
scopesListContainer: { scopesListContainer: {
textAlign: "center", textAlign: "center",
}, },