feat(oidc): pre-configured consent (#3118)
Allows users to pre-configure consent if enabled by the client configuration by selecting a checkbox during consent. Closes #2598pull/3142/head^2
parent
4503ac07be
commit
66a450ed38
|
@ -813,6 +813,11 @@ notifier:
|
||||||
## The policy to require for this client; one_factor or two_factor.
|
## The policy to require for this client; one_factor or two_factor.
|
||||||
# authorization_policy: two_factor
|
# authorization_policy: two_factor
|
||||||
|
|
||||||
|
## By default users cannot remember pre-configured consents. Setting this value to a period of time using a
|
||||||
|
## duration notation will enable users to remember consent for this client. The time configured is the amount
|
||||||
|
## of time the pre-configured consent is valid for granting new authorizations to the user.
|
||||||
|
# pre_configured_consent_duration:
|
||||||
|
|
||||||
## Audience this client is allowed to request.
|
## Audience this client is allowed to request.
|
||||||
# audience: []
|
# audience: []
|
||||||
|
|
||||||
|
|
|
@ -51,6 +51,7 @@ identity_providers:
|
||||||
sector_identifier: ''
|
sector_identifier: ''
|
||||||
public: false
|
public: false
|
||||||
authorization_policy: two_factor
|
authorization_policy: two_factor
|
||||||
|
pre_configured_consent_duration: ''
|
||||||
audience: []
|
audience: []
|
||||||
scopes:
|
scopes:
|
||||||
- openid
|
- openid
|
||||||
|
@ -402,6 +403,21 @@ required: no
|
||||||
|
|
||||||
The authorization policy for this client: either `one_factor` or `two_factor`.
|
The authorization policy for this client: either `one_factor` or `two_factor`.
|
||||||
|
|
||||||
|
#### pre_configured_consent_duration
|
||||||
|
<div markdown="1">
|
||||||
|
type: string (duration)
|
||||||
|
{: .label .label-config .label-purple }
|
||||||
|
required: no
|
||||||
|
{: .label .label-config .label-green }
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Configuring this enables users of this client to remember their consent as a pre-configured consent. The value is period
|
||||||
|
of time is in [duration notation format](../index.md#duration-notation-format). The period of time dictates how long a
|
||||||
|
users choice to remember the pre-configured consent lasts.
|
||||||
|
|
||||||
|
Pre-configured consents are only valid if the subject, client id are exactly the same and the requested scopes/audience
|
||||||
|
match exactly with the granted scopes/audience.
|
||||||
|
|
||||||
#### audience
|
#### audience
|
||||||
<div markdown="1">
|
<div markdown="1">
|
||||||
type: list(string)
|
type: list(string)
|
||||||
|
|
|
@ -813,6 +813,11 @@ notifier:
|
||||||
## The policy to require for this client; one_factor or two_factor.
|
## The policy to require for this client; one_factor or two_factor.
|
||||||
# authorization_policy: two_factor
|
# authorization_policy: two_factor
|
||||||
|
|
||||||
|
## By default users cannot remember pre-configured consents. Setting this value to a period of time using a
|
||||||
|
## duration notation will enable users to remember consent for this client. The time configured is the amount
|
||||||
|
## of time the pre-configured consent is valid for granting new authorizations to the user.
|
||||||
|
# pre_configured_consent_duration:
|
||||||
|
|
||||||
## Audience this client is allowed to request.
|
## Audience this client is allowed to request.
|
||||||
# audience: []
|
# audience: []
|
||||||
|
|
||||||
|
|
|
@ -47,8 +47,6 @@ type OpenIDConnectClientConfiguration struct {
|
||||||
SectorIdentifier url.URL `koanf:"sector_identifier"`
|
SectorIdentifier url.URL `koanf:"sector_identifier"`
|
||||||
Public bool `koanf:"public"`
|
Public bool `koanf:"public"`
|
||||||
|
|
||||||
Policy string `koanf:"authorization_policy"`
|
|
||||||
|
|
||||||
RedirectURIs []string `koanf:"redirect_uris"`
|
RedirectURIs []string `koanf:"redirect_uris"`
|
||||||
|
|
||||||
Audience []string `koanf:"audience"`
|
Audience []string `koanf:"audience"`
|
||||||
|
@ -58,6 +56,10 @@ type OpenIDConnectClientConfiguration struct {
|
||||||
ResponseModes []string `koanf:"response_modes"`
|
ResponseModes []string `koanf:"response_modes"`
|
||||||
|
|
||||||
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
|
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
|
||||||
|
|
||||||
|
Policy string `koanf:"authorization_policy"`
|
||||||
|
|
||||||
|
PreConfiguredConsentDuration *time.Duration `koanf:"pre_configured_consent_duration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultOpenIDConnectConfiguration contains defaults for OIDC.
|
// DefaultOpenIDConnectConfiguration contains defaults for OIDC.
|
||||||
|
|
|
@ -495,6 +495,7 @@ var ValidKeys = []string{
|
||||||
"identity_providers.oidc.clients[].public",
|
"identity_providers.oidc.clients[].public",
|
||||||
"identity_providers.oidc.clients[].redirect_uris",
|
"identity_providers.oidc.clients[].redirect_uris",
|
||||||
"identity_providers.oidc.clients[].authorization_policy",
|
"identity_providers.oidc.clients[].authorization_policy",
|
||||||
|
"identity_providers.oidc.clients[].pre_configured_consent_duration",
|
||||||
"identity_providers.oidc.clients[].scopes",
|
"identity_providers.oidc.clients[].scopes",
|
||||||
"identity_providers.oidc.clients[].audience",
|
"identity_providers.oidc.clients[].audience",
|
||||||
"identity_providers.oidc.clients[].grant_types",
|
"identity_providers.oidc.clients[].grant_types",
|
||||||
|
|
|
@ -3,6 +3,7 @@ package handlers
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/authelia/authelia/v4/internal/middlewares"
|
"github.com/authelia/authelia/v4/internal/middlewares"
|
||||||
"github.com/authelia/authelia/v4/internal/model"
|
"github.com/authelia/authelia/v4/internal/model"
|
||||||
|
@ -78,6 +79,17 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if body.PreConfigure {
|
||||||
|
if client.PreConfiguredConsentDuration == nil {
|
||||||
|
ctx.Logger.Warnf("Consent session with challenge id '%s' for user '%s': consent pre-configuration was requested and was ignored because it is not permitted on this client", consent.ChallengeID.String(), userSession.Username)
|
||||||
|
} else {
|
||||||
|
expiresAt := time.Now().Add(*client.PreConfiguredConsentDuration)
|
||||||
|
consent.ExpiresAt = &expiresAt
|
||||||
|
|
||||||
|
ctx.Logger.Debugf("Consent session with challenge id '%s' for user '%s': pre-configured and set to expire at %v", consent.ChallengeID.String(), userSession.Username, consent.ExpiresAt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
consent.GrantedScopes = consent.RequestedScopes
|
consent.GrantedScopes = consent.RequestedScopes
|
||||||
consent.GrantedAudience = consent.RequestedAudience
|
consent.GrantedAudience = consent.RequestedAudience
|
||||||
|
|
||||||
|
@ -87,7 +99,7 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
|
||||||
case reject:
|
case reject:
|
||||||
authorized = false
|
authorized = false
|
||||||
default:
|
default:
|
||||||
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb", userSession.Username)
|
ctx.Logger.Warnf("User '%s' tried to reply to consent with an unexpected verb '%s'", userSession.Username, body.AcceptOrReject)
|
||||||
ctx.ReplyBadRequest()
|
ctx.ReplyBadRequest()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
|
@ -18,8 +18,6 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
|
||||||
SectorIdentifier: config.SectorIdentifier.String(),
|
SectorIdentifier: config.SectorIdentifier.String(),
|
||||||
Public: config.Public,
|
Public: config.Public,
|
||||||
|
|
||||||
Policy: authorization.PolicyToLevel(config.Policy),
|
|
||||||
|
|
||||||
Audience: config.Audience,
|
Audience: config.Audience,
|
||||||
Scopes: config.Scopes,
|
Scopes: config.Scopes,
|
||||||
RedirectURIs: config.RedirectURIs,
|
RedirectURIs: config.RedirectURIs,
|
||||||
|
@ -28,6 +26,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
|
||||||
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
|
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
|
||||||
|
|
||||||
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
|
||||||
|
|
||||||
|
Policy: authorization.PolicyToLevel(config.Policy),
|
||||||
|
|
||||||
|
PreConfiguredConsentDuration: config.PreConfiguredConsentDuration,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mode := range config.ResponseModes {
|
for _, mode := range config.ResponseModes {
|
||||||
|
@ -57,6 +59,7 @@ func (c Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Cons
|
||||||
body := ConsentGetResponseBody{
|
body := ConsentGetResponseBody{
|
||||||
ClientID: c.ID,
|
ClientID: c.ID,
|
||||||
ClientDescription: c.Description,
|
ClientDescription: c.Description,
|
||||||
|
PreConfiguration: c.PreConfiguredConsentDuration != nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
if consent != nil {
|
if consent != nil {
|
||||||
|
|
|
@ -102,8 +102,6 @@ type Client struct {
|
||||||
SectorIdentifier string
|
SectorIdentifier string
|
||||||
Public bool
|
Public bool
|
||||||
|
|
||||||
Policy authorization.Level
|
|
||||||
|
|
||||||
Audience []string
|
Audience []string
|
||||||
Scopes []string
|
Scopes []string
|
||||||
RedirectURIs []string
|
RedirectURIs []string
|
||||||
|
@ -112,6 +110,10 @@ type Client struct {
|
||||||
ResponseModes []fosite.ResponseModeType
|
ResponseModes []fosite.ResponseModeType
|
||||||
|
|
||||||
UserinfoSigningAlgorithm string
|
UserinfoSigningAlgorithm string
|
||||||
|
|
||||||
|
Policy authorization.Level
|
||||||
|
|
||||||
|
PreConfiguredConsentDuration *time.Duration
|
||||||
}
|
}
|
||||||
|
|
||||||
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.
|
// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them.
|
||||||
|
@ -132,12 +134,14 @@ type ConsentGetResponseBody struct {
|
||||||
ClientDescription string `json:"client_description"`
|
ClientDescription string `json:"client_description"`
|
||||||
Scopes []string `json:"scopes"`
|
Scopes []string `json:"scopes"`
|
||||||
Audience []string `json:"audience"`
|
Audience []string `json:"audience"`
|
||||||
|
PreConfiguration bool `json:"pre_configuration"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
|
||||||
type ConsentPostRequestBody struct {
|
type ConsentPostRequestBody struct {
|
||||||
ClientID string `json:"client_id"`
|
ClientID string `json:"client_id"`
|
||||||
AcceptOrReject string `json:"accept_or_reject"`
|
AcceptOrReject string `json:"accept_or_reject"`
|
||||||
|
PreConfigure bool `json:"pre_configure"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
|
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
|
||||||
|
|
|
@ -62,6 +62,8 @@
|
||||||
"Must have at least one special character": "Must have at least one special character",
|
"Must have at least one special character": "Must have at least one special character",
|
||||||
"Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length",
|
"Must be at least {{len}} characters in length": "Must be at least {{len}} characters in length",
|
||||||
"Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length",
|
"Must not be more than {{len}} characters in length": "Must not be more than {{len}} characters in length",
|
||||||
|
"This saves this consent as a pre-configured consent for future use": "This saves this consent as a pre-configured consent for future use",
|
||||||
|
"Remember Consent": "Remember Consent",
|
||||||
"Consent Request": "Consent Request",
|
"Consent Request": "Consent Request",
|
||||||
"Client ID": "Client ID: {{client_id}}"
|
"Client ID": "Client ID: {{client_id}}"
|
||||||
}
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { useRemoteCall } from "@hooks/RemoteCall";
|
import { useRemoteCall } from "@hooks/RemoteCall";
|
||||||
import { getRequestedScopes } from "@services/Consent";
|
import { getConsentResponse } from "@services/Consent";
|
||||||
|
|
||||||
export function useRequestedScopes() {
|
export function useConsentResponse() {
|
||||||
return useRemoteCall(getRequestedScopes, []);
|
return useRemoteCall(getConsentResponse, []);
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { Post, Get } from "@services/Client";
|
||||||
interface ConsentPostRequestBody {
|
interface ConsentPostRequestBody {
|
||||||
client_id: string;
|
client_id: string;
|
||||||
accept_or_reject: "accept" | "reject";
|
accept_or_reject: "accept" | "reject";
|
||||||
|
pre_configure: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ConsentPostResponseBody {
|
interface ConsentPostResponseBody {
|
||||||
|
@ -15,18 +16,23 @@ interface ConsentGetResponseBody {
|
||||||
client_description: string;
|
client_description: string;
|
||||||
scopes: string[];
|
scopes: string[];
|
||||||
audience: string[];
|
audience: string[];
|
||||||
|
pre_configuration: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getRequestedScopes() {
|
export function getConsentResponse() {
|
||||||
return Get<ConsentGetResponseBody>(ConsentPath);
|
return Get<ConsentGetResponseBody>(ConsentPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function acceptConsent(clientID: string) {
|
export function acceptConsent(clientID: string, preConfigure: boolean) {
|
||||||
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "accept" };
|
const body: ConsentPostRequestBody = {
|
||||||
|
client_id: clientID,
|
||||||
|
accept_or_reject: "accept",
|
||||||
|
pre_configure: preConfigure,
|
||||||
|
};
|
||||||
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function rejectConsent(clientID: string) {
|
export function rejectConsent(clientID: string) {
|
||||||
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject" };
|
const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "reject", pre_configure: false };
|
||||||
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
return Post<ConsentPostResponseBody>(ConsentPath, body);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect, Fragment, ReactNode } from "react";
|
import React, { useEffect, Fragment, ReactNode, useState } from "react";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
|
@ -10,13 +10,15 @@ import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Typography,
|
Typography,
|
||||||
makeStyles,
|
makeStyles,
|
||||||
|
Checkbox,
|
||||||
|
FormControlLabel,
|
||||||
} from "@material-ui/core";
|
} 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 { useTranslation } from "react-i18next";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
import { IndexRoute } from "@constants/Routes";
|
import { IndexRoute } from "@constants/Routes";
|
||||||
import { useRequestedScopes } from "@hooks/Consent";
|
import { useConsentResponse } from "@hooks/Consent";
|
||||||
import { useNotifications } from "@hooks/NotificationsContext";
|
import { useNotifications } from "@hooks/NotificationsContext";
|
||||||
import { useRedirector } from "@hooks/Redirector";
|
import { useRedirector } from "@hooks/Redirector";
|
||||||
import { useUserInfoGET } from "@hooks/UserInfo";
|
import { useUserInfoGET } from "@hooks/UserInfo";
|
||||||
|
@ -46,9 +48,15 @@ const ConsentView = function (props: Props) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const redirect = useRedirector();
|
const redirect = useRedirector();
|
||||||
const { createErrorNotification, resetNotification } = useNotifications();
|
const { createErrorNotification, resetNotification } = useNotifications();
|
||||||
const [resp, fetch, , err] = useRequestedScopes();
|
const [resp, fetch, , err] = useConsentResponse();
|
||||||
const { t: translate } = useTranslation();
|
const { t: translate } = useTranslation();
|
||||||
|
|
||||||
|
const [preConfigure, setPreConfigure] = useState(false);
|
||||||
|
|
||||||
|
const handlePreConfigureChanged = () => {
|
||||||
|
setPreConfigure((preConfigure) => !preConfigure);
|
||||||
|
};
|
||||||
|
|
||||||
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET();
|
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -92,7 +100,7 @@ const ConsentView = function (props: Props) {
|
||||||
if (!resp) {
|
if (!resp) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const res = await acceptConsent(resp.client_id);
|
const res = await acceptConsent(resp.client_id, preConfigure);
|
||||||
if (res.redirect_uri) {
|
if (res.redirect_uri) {
|
||||||
redirect(res.redirect_uri);
|
redirect(res.redirect_uri);
|
||||||
} else {
|
} else {
|
||||||
|
@ -154,6 +162,30 @@ const ConsentView = function (props: Props) {
|
||||||
</List>
|
</List>
|
||||||
</div>
|
</div>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
{resp?.pre_configuration ? (
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Tooltip
|
||||||
|
title={
|
||||||
|
translate("This saves this consent as a pre-configured consent for future use") ||
|
||||||
|
"This saves this consent as a pre-configured consent for future use"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
id="pre-configure"
|
||||||
|
checked={preConfigure}
|
||||||
|
onChange={handlePreConfigureChanged}
|
||||||
|
value="preConfigure"
|
||||||
|
color="primary"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={classes.preConfigure}
|
||||||
|
label={translate("Remember Consent")}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
</Grid>
|
||||||
|
) : null}
|
||||||
<Grid item xs={12}>
|
<Grid item xs={12}>
|
||||||
<Grid container spacing={1}>
|
<Grid container spacing={1}>
|
||||||
<Grid item xs={6}>
|
<Grid item xs={6}>
|
||||||
|
@ -226,6 +258,7 @@ const useStyles = makeStyles((theme) => ({
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginRight: theme.spacing(2),
|
marginRight: theme.spacing(2),
|
||||||
},
|
},
|
||||||
|
preConfigure: {},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export default ConsentView;
|
export default ConsentView;
|
||||||
|
|
Loading…
Reference in New Issue