diff --git a/config.template.yml b/config.template.yml
index 9e46ed6f3..4e45513b8 100644
--- a/config.template.yml
+++ b/config.template.yml
@@ -813,6 +813,11 @@ notifier:
## The policy to require for this client; one_factor or 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: []
diff --git a/docs/configuration/identity-providers/oidc.md b/docs/configuration/identity-providers/oidc.md
index 55a8c2819..65fa3ba0e 100644
--- a/docs/configuration/identity-providers/oidc.md
+++ b/docs/configuration/identity-providers/oidc.md
@@ -51,6 +51,7 @@ identity_providers:
sector_identifier: ''
public: false
authorization_policy: two_factor
+ pre_configured_consent_duration: ''
audience: []
scopes:
- openid
@@ -402,6 +403,21 @@ required: no
The authorization policy for this client: either `one_factor` or `two_factor`.
+#### pre_configured_consent_duration
+
+type: string (duration)
+{: .label .label-config .label-purple }
+required: no
+{: .label .label-config .label-green }
+
+
+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
type: list(string)
diff --git a/internal/configuration/config.template.yml b/internal/configuration/config.template.yml
index 9e46ed6f3..4e45513b8 100644
--- a/internal/configuration/config.template.yml
+++ b/internal/configuration/config.template.yml
@@ -813,6 +813,11 @@ notifier:
## The policy to require for this client; one_factor or 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: []
diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go
index 69c82a3ea..0bfb969d9 100644
--- a/internal/configuration/schema/identity_providers.go
+++ b/internal/configuration/schema/identity_providers.go
@@ -47,8 +47,6 @@ type OpenIDConnectClientConfiguration struct {
SectorIdentifier url.URL `koanf:"sector_identifier"`
Public bool `koanf:"public"`
- Policy string `koanf:"authorization_policy"`
-
RedirectURIs []string `koanf:"redirect_uris"`
Audience []string `koanf:"audience"`
@@ -58,6 +56,10 @@ type OpenIDConnectClientConfiguration struct {
ResponseModes []string `koanf:"response_modes"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
+
+ Policy string `koanf:"authorization_policy"`
+
+ PreConfiguredConsentDuration *time.Duration `koanf:"pre_configured_consent_duration"`
}
// DefaultOpenIDConnectConfiguration contains defaults for OIDC.
diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go
index cf7390030..0698c8995 100644
--- a/internal/configuration/validator/const.go
+++ b/internal/configuration/validator/const.go
@@ -495,6 +495,7 @@ var ValidKeys = []string{
"identity_providers.oidc.clients[].public",
"identity_providers.oidc.clients[].redirect_uris",
"identity_providers.oidc.clients[].authorization_policy",
+ "identity_providers.oidc.clients[].pre_configured_consent_duration",
"identity_providers.oidc.clients[].scopes",
"identity_providers.oidc.clients[].audience",
"identity_providers.oidc.clients[].grant_types",
diff --git a/internal/handlers/handler_oidc_consent.go b/internal/handlers/handler_oidc_consent.go
index af41dfb59..35e163224 100644
--- a/internal/handlers/handler_oidc_consent.go
+++ b/internal/handlers/handler_oidc_consent.go
@@ -3,6 +3,7 @@ package handlers
import (
"encoding/json"
"fmt"
+ "time"
"github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/model"
@@ -78,6 +79,17 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
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.GrantedAudience = consent.RequestedAudience
@@ -87,7 +99,7 @@ func OpenIDConnectConsentPOST(ctx *middlewares.AutheliaCtx) {
case reject:
authorized = false
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()
return
diff --git a/internal/oidc/client.go b/internal/oidc/client.go
index 2b7a04e46..0be6ce4f0 100644
--- a/internal/oidc/client.go
+++ b/internal/oidc/client.go
@@ -18,8 +18,6 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
SectorIdentifier: config.SectorIdentifier.String(),
Public: config.Public,
- Policy: authorization.PolicyToLevel(config.Policy),
-
Audience: config.Audience,
Scopes: config.Scopes,
RedirectURIs: config.RedirectURIs,
@@ -28,6 +26,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
ResponseModes: []fosite.ResponseModeType{fosite.ResponseModeDefault},
UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm,
+
+ Policy: authorization.PolicyToLevel(config.Policy),
+
+ PreConfiguredConsentDuration: config.PreConfiguredConsentDuration,
}
for _, mode := range config.ResponseModes {
@@ -57,6 +59,7 @@ func (c Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Cons
body := ConsentGetResponseBody{
ClientID: c.ID,
ClientDescription: c.Description,
+ PreConfiguration: c.PreConfiguredConsentDuration != nil,
}
if consent != nil {
diff --git a/internal/oidc/types.go b/internal/oidc/types.go
index a144295e6..43a79a3ec 100644
--- a/internal/oidc/types.go
+++ b/internal/oidc/types.go
@@ -102,8 +102,6 @@ type Client struct {
SectorIdentifier string
Public bool
- Policy authorization.Level
-
Audience []string
Scopes []string
RedirectURIs []string
@@ -112,6 +110,10 @@ type Client struct {
ResponseModes []fosite.ResponseModeType
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.
@@ -132,12 +134,14 @@ type ConsentGetResponseBody struct {
ClientDescription string `json:"client_description"`
Scopes []string `json:"scopes"`
Audience []string `json:"audience"`
+ PreConfiguration bool `json:"pre_configuration"`
}
// ConsentPostRequestBody schema of the request body of the consent POST endpoint.
type ConsentPostRequestBody struct {
ClientID string `json:"client_id"`
AcceptOrReject string `json:"accept_or_reject"`
+ PreConfigure bool `json:"pre_configure"`
}
// ConsentPostResponseBody schema of the response body of the consent POST endpoint.
diff --git a/internal/server/locales/en/portal.json b/internal/server/locales/en/portal.json
index e57e4677f..ed845d49a 100644
--- a/internal/server/locales/en/portal.json
+++ b/internal/server/locales/en/portal.json
@@ -62,6 +62,8 @@
"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 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",
"Client ID": "Client ID: {{client_id}}"
}
\ No newline at end of file
diff --git a/web/src/hooks/Consent.ts b/web/src/hooks/Consent.ts
index 4c7c7fed7..08300c6e6 100644
--- a/web/src/hooks/Consent.ts
+++ b/web/src/hooks/Consent.ts
@@ -1,6 +1,6 @@
import { useRemoteCall } from "@hooks/RemoteCall";
-import { getRequestedScopes } from "@services/Consent";
+import { getConsentResponse } from "@services/Consent";
-export function useRequestedScopes() {
- return useRemoteCall(getRequestedScopes, []);
+export function useConsentResponse() {
+ return useRemoteCall(getConsentResponse, []);
}
diff --git a/web/src/services/Consent.ts b/web/src/services/Consent.ts
index befecc1c5..36601feb5 100644
--- a/web/src/services/Consent.ts
+++ b/web/src/services/Consent.ts
@@ -4,6 +4,7 @@ import { Post, Get } from "@services/Client";
interface ConsentPostRequestBody {
client_id: string;
accept_or_reject: "accept" | "reject";
+ pre_configure: boolean;
}
interface ConsentPostResponseBody {
@@ -15,18 +16,23 @@ interface ConsentGetResponseBody {
client_description: string;
scopes: string[];
audience: string[];
+ pre_configuration: boolean;
}
-export function getRequestedScopes() {
+export function getConsentResponse() {
return Get(ConsentPath);
}
-export function acceptConsent(clientID: string) {
- const body: ConsentPostRequestBody = { client_id: clientID, accept_or_reject: "accept" };
+export function acceptConsent(clientID: string, preConfigure: boolean) {
+ const body: ConsentPostRequestBody = {
+ client_id: clientID,
+ accept_or_reject: "accept",
+ pre_configure: preConfigure,
+ };
return Post(ConsentPath, body);
}
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(ConsentPath, body);
}
diff --git a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx
index f52119cf0..965bdab2e 100644
--- a/web/src/views/LoginPortal/ConsentView/ConsentView.tsx
+++ b/web/src/views/LoginPortal/ConsentView/ConsentView.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, Fragment, ReactNode } from "react";
+import React, { useEffect, Fragment, ReactNode, useState } from "react";
import {
Button,
@@ -10,13 +10,15 @@ import {
Tooltip,
Typography,
makeStyles,
+ Checkbox,
+ FormControlLabel,
} 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 { IndexRoute } from "@constants/Routes";
-import { useRequestedScopes } from "@hooks/Consent";
+import { useConsentResponse } from "@hooks/Consent";
import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirector } from "@hooks/Redirector";
import { useUserInfoGET } from "@hooks/UserInfo";
@@ -46,9 +48,15 @@ const ConsentView = function (props: Props) {
const navigate = useNavigate();
const redirect = useRedirector();
const { createErrorNotification, resetNotification } = useNotifications();
- const [resp, fetch, , err] = useRequestedScopes();
+ const [resp, fetch, , err] = useConsentResponse();
const { t: translate } = useTranslation();
+ const [preConfigure, setPreConfigure] = useState(false);
+
+ const handlePreConfigureChanged = () => {
+ setPreConfigure((preConfigure) => !preConfigure);
+ };
+
const [userInfo, fetchUserInfo, , fetchUserInfoError] = useUserInfoGET();
useEffect(() => {
@@ -92,7 +100,7 @@ const ConsentView = function (props: Props) {
if (!resp) {
return;
}
- const res = await acceptConsent(resp.client_id);
+ const res = await acceptConsent(resp.client_id, preConfigure);
if (res.redirect_uri) {
redirect(res.redirect_uri);
} else {
@@ -154,6 +162,30 @@ const ConsentView = function (props: Props) {
+ {resp?.pre_configuration ? (
+
+
+
+ }
+ className={classes.preConfigure}
+ label={translate("Remember Consent")}
+ />
+
+
+ ) : null}
@@ -226,6 +258,7 @@ const useStyles = makeStyles((theme) => ({
textAlign: "center",
marginRight: theme.spacing(2),
},
+ preConfigure: {},
}));
export default ConsentView;