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
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 |
|:---------:|:-------------:|:------------------:|:----------------------:|
| 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,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.
| JWT Field | JWT Type | Authelia Attribute | Description |
|:---------:|:--------:|:------------------:|:----------------------:|
| Claim | JWT Type | Authelia Attribute | Description |
|:-----:|:--------:|:------------------:|:----------------------:|
| name | string | display_name | The users display name |
## Endpoint Implementations

View File

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

View File

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

View File

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

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 {
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"`
Scopes []string `json:"scopes"`
Audience []string `json:"audience"`
}
// WellKnownConfiguration is the OIDC well known config struct.

View File

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

View File

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

View File

@ -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() {

View File

@ -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 <AccountBox />;
@ -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) {
<LoginLayout id="consent-stage" title={`Permissions Request`} showBrand>
<Grid container>
<Grid item xs={12}>
<div style={{ textAlign: "left" }}>
The application
<b>{` ${resp?.client_description} (${resp?.client_id}) `}</b>
is requesting the following permissions
<div>
{resp !== undefined && resp.client_description !== "" ? (
<Tooltip title={"Client ID: " + resp.client_id}>
<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>
</Grid>
<Grid item xs={12}>
<div>{translate("The above application is requesting the following permissions")}:</div>
</Grid>
<Grid item xs={12}>
<div className={classes.scopesListContainer}>
<List className={classes.scopesList}>
{resp?.scopes.map((s) => (
<Tooltip title={"Scope " + s.name}>
<ListItem id={"scope-" + s.name} dense>
<ListItemIcon>{showListItemAvatar(s.name)}</ListItemIcon>
<ListItemText primary={s.description} />
{resp?.scopes.map((scope: string) => (
<Tooltip title={"Scope " + scope}>
<ListItem id={"scope-" + scope} dense>
<ListItemIcon>{scopeNameToAvatar(scope)}</ListItemIcon>
<ListItemText primary={translateScopeNameToDescription(scope)} />
</ListItem>
</Tooltip>
))}
@ -108,7 +146,7 @@ const ConsentView = function (props: Props) {
color="primary"
variant="contained"
>
Accept
{translate("Accept")}
</Button>
</Grid>
<Grid item xs={6}>
@ -120,7 +158,7 @@ const ConsentView = function (props: Props) {
color="secondary"
variant="contained"
>
Deny
{translate("Deny")}
</Button>
</Grid>
</Grid>
@ -138,6 +176,9 @@ const useStyles = makeStyles((theme) => ({
display: "block",
justifyContent: "center",
},
clientDescription: {
fontWeight: 600,
},
scopesListContainer: {
textAlign: "center",
},