Merge remote-tracking branch 'origin/master' into feat-settings-ui

# Conflicts:
#	api/openapi.yml
#	web/src/views/DeviceRegistration/RegisterWebauthn.tsx
#	web/src/views/LoginPortal/SecondFactor/WebauthnMethod.tsx
pull/4806/head
James Elliott 2023-01-07 11:48:22 +11:00
commit 49d421e910
No known key found for this signature in database
GPG Key ID: 0F1C4A096E857E49
97 changed files with 3200 additions and 1765 deletions

View File

@ -9,6 +9,7 @@ yaml-files:
- '.yamllint' - '.yamllint'
ignore: | ignore: |
api/openapi.yml
docs/pnpm-lock.yaml docs/pnpm-lock.yaml
internal/configuration/test_resources/config_bad_quoting.yml internal/configuration/test_resources/config_bad_quoting.yml
web/pnpm-lock.yaml web/pnpm-lock.yaml

View File

@ -1,5 +1,5 @@
<p align="center"> <p align="center">
<img src="./docs/static/images/authelia-title.png" width="350" title="Authelia"> <img src="https://www.authelia.com/images/authelia-title.png" width="350" title="Authelia">
</p> </p>
[![Build](https://img.shields.io/buildkite/d6543d3ece3433f46dbe5fd9fcfaf1f68a6dbc48eb1048bc22/master?logo=buildkite&style=flat-square&color=brightgreen)](https://buildkite.com/authelia/authelia) [![Build](https://img.shields.io/buildkite/d6543d3ece3433f46dbe5fd9fcfaf1f68a6dbc48eb1048bc22/master?logo=buildkite&style=flat-square&color=brightgreen)](https://buildkite.com/authelia/authelia)
@ -24,7 +24,7 @@ Documentation is available at [https://www.authelia.com/](https://www.authelia.c
The following is a simple diagram of the architecture: The following is a simple diagram of the architecture:
<p align="center" style="margin:50px"> <p align="center" style="margin:50px">
<img src="./docs/static/images/archi.png"/> <img src="https://www.authelia.com/images/archi.png"/>
</p> </p>
**Authelia** can be installed as a standalone service from the [AUR](https://aur.archlinux.org/packages/authelia/), **Authelia** can be installed as a standalone service from the [AUR](https://aur.archlinux.org/packages/authelia/),
@ -38,15 +38,15 @@ Deployment can be orchestrated via the Helm [Chart](https://charts.authelia.com)
and ingress configurations. and ingress configurations.
<p align="center"> <p align="center">
<img src="./docs/static/images/logos/kubernetes.png" height="100"/> <img src="https://www.authelia.com/images/logos/kubernetes.png" height="100"/>
<img src="./docs/static/images/logos/docker.logo.png" width="100"> <img src="https://www.authelia.com/images/logos/docker.logo.png" width="100">
</p> </p>
Here is what Authelia's portal looks like: Here is what Authelia's portal looks like:
<p align="center"> <p align="center">
<img src="./docs/static/images/1FA.png" width="400" /> <img src="https://www.authelia.com/images/1FA.png" width="400" />
<img src="./docs/static/images/2FA-METHODS.png" width="400" /> <img src="https://www.authelia.com/images/2FA-METHODS.png" width="400" />
</p> </p>
## Features summary ## Features summary
@ -92,11 +92,11 @@ If you want to know more about the roadmap, follow [Roadmap](https://www.autheli
Authelia works in combination with [nginx], [Traefik], [Caddy], [Skipper], [Envoy], or [HAProxy]. Authelia works in combination with [nginx], [Traefik], [Caddy], [Skipper], [Envoy], or [HAProxy].
<p align="center"> <p align="center">
<img src="./docs/static/images/logos/nginx.png" height="50"/> <img src="https://www.authelia.com/images/logos/nginx.png" height="50"/>
<img src="./docs/static/images/logos/traefik.png" height="50"/> <img src="https://www.authelia.com/images/logos/traefik.png" height="50"/>
<img src="./docs/static/images/logos/caddy.png" height="50"/> <img src="https://www.authelia.com/images/logos/caddy.png" height="50"/>
<img src="./docs/static/images/logos/envoy.png" height="50"/> <img src="https://www.authelia.com/images/logos/envoy.png" height="50"/>
<img src="./docs/static/images/logos/haproxy.png" height="50"/> <img src="https://www.authelia.com/images/logos/haproxy.png" height="50"/>
</p> </p>
## Getting Started ## Getting Started
@ -330,17 +330,17 @@ Authelia.
#### Balto #### Balto
Thank you to [<img src="./docs/static/images/logos/balto.svg" alt="Balto" width="32"> Balto](https://www.getbalto.com/) Thank you to [<img src="https://www.authelia.com/images/logos/balto.svg" alt="Balto" width="32"> Balto](https://www.getbalto.com/)
for hosting our apt repository. for hosting our apt repository.
#### JetBrains #### JetBrains
Thank you to [<img src="./docs/static/images/logos/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](https://www.jetbrains.com/?from=Authelia) Thank you to [<img src="https://www.authelia.com/images/logos/jetbrains.svg" alt="JetBrains" width="32"> JetBrains](https://www.jetbrains.com/?from=Authelia)
for providing us with free licenses to their great tools. for providing us with free licenses to their great tools.
* [<img src="./docs/static/images/logos/intellij-idea.svg" alt="IDEA" width="32"> IDEA](http://www.jetbrains.com/idea/) * [<img src="https://www.authelia.com/images/logos/intellij-idea.svg" alt="IDEA" width="32"> IDEA](http://www.jetbrains.com/idea/)
* [<img src="./docs/static/images/logos/goland.svg" alt="GoLand" width="32"> GoLand](http://www.jetbrains.com/go/) * [<img src="https://www.authelia.com/images/logos/goland.svg" alt="GoLand" width="32"> GoLand](http://www.jetbrains.com/go/)
* [<img src="./docs/static/images/logos/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/) * [<img src="https://www.authelia.com/images/logos/webstorm.svg" alt="WebStorm" width="32"> WebStorm](http://www.jetbrains.com/webstorm/)
#### Microsoft #### Microsoft
@ -348,9 +348,9 @@ Our pipeline agents which we rely on for productivity are hosted on [Azure](http
and our [git repositories](https://github.com/authelia) are hosted on [GitHub](https://github.com/?from=Authela) and our [git repositories](https://github.com/authelia) are hosted on [GitHub](https://github.com/?from=Authela)
which are both [Microsoft](https://www.microsoft.com/?from=Authelia) products. which are both [Microsoft](https://www.microsoft.com/?from=Authelia) products.
[<img src="./docs/static/images/logos/microsoft.svg" alt="microsoft" height="32">](https://www.microsoft.com/?from=Authelia) [<img src="https://www.authelia.com/images/logos/microsoft.svg" alt="microsoft" height="32">](https://www.microsoft.com/?from=Authelia)
[<img src="./docs/static/images/logos/azure.svg" alt="Azure" height="32">](https://azure.microsoft.com/?from=Authelia) [<img src="https://www.authelia.com/images/logos/azure.svg" alt="Azure" height="32">](https://azure.microsoft.com/?from=Authelia)
### Open Collective ### Open Collective

View File

@ -1,4 +1,3 @@
# yamllint disable rule:line-length
--- ---
openapi: 3.0.3 openapi: 3.0.3
info: info:
@ -22,18 +21,24 @@ tags:
description: Configuration, health and state endpoints description: Configuration, health and state endpoints
- name: Authentication - name: Authentication
description: Authentication and verification endpoints description: Authentication and verification endpoints
{{- if .PasswordReset }}
- name: Password Reset - name: Password Reset
description: Password reset endpoints description: Password reset endpoints
- name: User Information - name: User Information
description: User configuration endpoints description: User configuration endpoints
{{- end }}
{{- if (or .TOTP .Webauthn .Duo) }}
- name: Second Factor - name: Second Factor
description: TOTP, Webauthn and Duo endpoints description: TOTP, Webauthn and Duo endpoints
externalDocs: externalDocs:
url: https://www.authelia.com/configuration/second-factor/introduction/ url: https://www.authelia.com/configuration/second-factor/introduction/
{{- end }}
{{- if .OpenIDConnect }}
- name: OpenID Connect 1.0 - name: OpenID Connect 1.0
description: OpenID Connect 1.0 and OAuth 2.0 Endpoints description: OpenID Connect 1.0 and OAuth 2.0 Endpoints
externalDocs: externalDocs:
url: https://www.authelia.com/integration/openid-connect/introduction/ url: https://www.authelia.com/integration/openid-connect/introduction/
{{- end }}
paths: paths:
/api/configuration: /api/configuration:
get: get:
@ -97,280 +102,8 @@ paths:
schema: schema:
$ref: '#/components/schemas/handlers.StateResponse' $ref: '#/components/schemas/handlers.StateResponse'
/api/verify: /api/verify:
get: {{- range $method := list "get" "head" "options" "post" "put" "patch" "delete" "trace" }}
tags: {{ $method }}:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
head:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
options:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
post:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
put:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
patch:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
delete:
tags:
- Authentication
summary: Verification
description: >
The verify endpoint provides the ability to verify if a user has the necessary permissions to access a specified
domain.
parameters:
- $ref: '#/components/parameters/originalURLParam'
- $ref: '#/components/parameters/forwardedMethodParam'
- $ref: '#/components/parameters/authParam'
responses:
"200":
description: Successful Operation
headers:
remote-user:
description: Username
schema:
type: string
example: john
remote-name:
description: Name
schema:
type: string
example: John Doe
remote-email:
description: Email
schema:
type: string
example: john.doe@authelia.com
remote-groups:
description: Comma separated list of Groups
schema:
type: string
example: admin,devs
"401":
description: Unauthorized
security:
- authelia_auth: []
trace:
tags: tags:
- Authentication - Authentication
summary: Verification summary: Verification
@ -409,6 +142,7 @@ paths:
description: Unauthorized description: Unauthorized
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }}
/api/firstfactor: /api/firstfactor:
post: post:
tags: tags:
@ -477,6 +211,7 @@ paths:
$ref: '#/components/schemas/handlers.logoutResponseBody' $ref: '#/components/schemas/handlers.logoutResponseBody'
security: security:
- authelia_auth: [] - authelia_auth: []
{{- if .PasswordReset }}
/api/reset-password/identity/start: /api/reset-password/identity/start:
post: post:
tags: tags:
@ -494,7 +229,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/handlers.resetPasswordStep1RequestBody' $ref: '#/components/schemas/handlers.PasswordResetStep1RequestBody'
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -546,7 +281,7 @@ paths:
content: content:
application/json: application/json:
schema: schema:
$ref: '#/components/schemas/handlers.resetPasswordStep2RequestBody' $ref: '#/components/schemas/handlers.PasswordResetStep2RequestBody'
responses: responses:
"200": "200":
description: Successful Operation description: Successful Operation
@ -556,6 +291,7 @@ paths:
$ref: '#/components/schemas/middlewares.OkResponse' $ref: '#/components/schemas/middlewares.OkResponse'
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }}
/api/user/info: /api/user/info:
get: get:
tags: tags:
@ -593,25 +329,6 @@ paths:
description: Forbidden description: Forbidden
security: security:
- authelia_auth: [] - authelia_auth: []
/api/user/info/totp:
get:
tags:
- User Information
summary: User TOTP Configuration
description: >
The user TOTP info endpoint provides information necessary to display the TOTP component to validate their
TOTP input such as the period/frequency and number of digits.
responses:
"200":
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/handlers.UserInfoTOTP'
"403":
description: Forbidden
security:
- authelia_auth: []
/api/user/info/2fa_method: /api/user/info/2fa_method:
post: post:
tags: tags:
@ -634,6 +351,26 @@ paths:
description: Forbidden description: Forbidden
security: security:
- authelia_auth: [] - authelia_auth: []
{{- if .TOTP }}
/api/user/info/totp:
get:
tags:
- User Information
summary: User TOTP Configuration
description: >
The user TOTP info endpoint provides information necessary to display the TOTP component to validate their
TOTP input such as the period/frequency and number of digits.
responses:
"200":
description: Successful Operation
content:
application/json:
schema:
$ref: '#/components/schemas/handlers.UserInfoTOTP'
"403":
description: Forbidden
security:
- authelia_auth: []
/api/secondfactor/totp/identity/start: /api/secondfactor/totp/identity/start:
post: post:
tags: tags:
@ -706,6 +443,8 @@ paths:
$ref: '#/components/schemas/middlewares.ErrorResponse' $ref: '#/components/schemas/middlewares.ErrorResponse'
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }}
{{- if .Webauthn }}
/api/secondfactor/webauthn/assertion: /api/secondfactor/webauthn/assertion:
get: get:
tags: tags:
@ -851,6 +590,8 @@ paths:
- authelia_auth: [] - authelia_auth: []
parameters: parameters:
- $ref: '#/components/parameters/deviceID' - $ref: '#/components/parameters/deviceID'
{{- end }}
{{- if .Duo }}
/api/secondfactor/duo: /api/secondfactor/duo:
post: post:
tags: tags:
@ -914,6 +655,8 @@ paths:
description: Unauthorized description: Unauthorized
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }}
{{- if .OpenIDConnect }}
/.well-known/openid-configuration: /.well-known/openid-configuration:
get: get:
tags: tags:
@ -1428,6 +1171,7 @@ paths:
description: Forbidden description: Forbidden
security: security:
- authelia_auth: [] - authelia_auth: []
{{- end }}
components: components:
parameters: parameters:
deviceID: deviceID:
@ -1655,7 +1399,8 @@ components:
redirect: redirect:
type: string type: string
example: https://home.example.com example: https://home.example.com
handlers.resetPasswordStep1RequestBody: {{- if .PasswordReset }}
handlers.PasswordResetStep1RequestBody:
required: required:
- username - username
type: object type: object
@ -1663,7 +1408,7 @@ components:
username: username:
type: string type: string
example: john example: john
handlers.resetPasswordStep2RequestBody: handlers.PasswordResetStep2RequestBody:
required: required:
- password - password
type: object type: object
@ -1671,6 +1416,8 @@ components:
password: password:
type: string type: string
example: password example: password
{{- end }}
{{- if .Duo }}
handlers.bodySignDuoRequest: handlers.bodySignDuoRequest:
type: object type: object
properties: properties:
@ -1687,23 +1434,7 @@ components:
format: uuid format: uuid
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$' pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c" example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
handlers.bodySignTOTPRequest: {{- end }}
type: object
properties:
token:
type: string
example: "123456"
targetURL:
type: string
example: https://secure.example.com
workflow:
type: string
example: openid_connect
workflowID:
type: string
format: uuid
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
handlers.StateResponse: handlers.StateResponse:
type: object type: object
properties: properties:
@ -1722,7 +1453,24 @@ components:
default_redirection_url: default_redirection_url:
type: string type: string
example: https://home.example.com example: https://home.example.com
handlers.TOTPKeyResponse: middlewares.ErrorResponse:
type: object
properties:
status:
type: string
example: KO
message:
type: string
example: Authentication failed, please retry later.
middlewares.IdentityVerificationFinishBody:
required:
- token
type: object
properties:
token:
type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY
middlewares.OkResponse:
type: object type: object
properties: properties:
status: status:
@ -1730,13 +1478,6 @@ components:
example: OK example: OK
data: data:
type: object type: object
properties:
base32_secret:
type: string
example: 5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
otpauth_url:
type: string
example: otpauth://totp/auth.example.com:john?algorithm=SHA1&digits=6&issuer=auth.example.com&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
handlers.UserInfo: handlers.UserInfo:
type: object type: object
properties: properties:
@ -1765,6 +1506,19 @@ components:
has_duo: has_duo:
type: boolean type: boolean
example: true example: true
handlers.UserInfo.MethodBody:
required:
- method
type: object
properties:
method:
type: string
enum:
- "totp"
- "webauthn"
- "mobile_push"
example: totp
{{- if .TOTP }}
handlers.UserInfoTOTP: handlers.UserInfoTOTP:
type: object type: object
properties: properties:
@ -1784,36 +1538,24 @@ components:
description: The number of digits defined in the users TOTP configuration description: The number of digits defined in the users TOTP configuration
type: integer type: integer
example: 6 example: 6
handlers.UserInfo.MethodBody: handlers.bodySignTOTPRequest:
required:
- method
type: object
properties:
method:
type: string
enum:
- "totp"
- "webauthn"
- "mobile_push"
example: totp
middlewares.ErrorResponse:
type: object
properties:
status:
type: string
example: KO
message:
type: string
example: Authentication failed, please retry later.
middlewares.IdentityVerificationFinishBody:
required:
- token
type: object type: object
properties: properties:
token: token:
type: string type: string
example: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2MDc5MjU1OTYsImlzcyI6IkF1dGhlbGlhIiwiYWN0aW9uIjoiUmVzZXRQYXNzd29yZCIsInVzZXJuYW1lIjoiQW1pciJ9.636yqRrUCGCe4jsMCsonleX5CYWHncYqZum-YYb6VaY example: "123456"
middlewares.OkResponse: targetURL:
type: string
example: https://secure.example.com
workflow:
type: string
example: openid_connect
workflowID:
type: string
format: uuid
pattern: '^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$'
example: "3ebcfbc5-b0fd-4ee0-9d3c-080ae1e7298c"
handlers.TOTPKeyResponse:
type: object type: object
properties: properties:
status: status:
@ -1821,6 +1563,15 @@ components:
example: OK example: OK
data: data:
type: object type: object
properties:
base32_secret:
type: string
example: 5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
otpauth_url:
type: string
example: otpauth://totp/auth.example.com:john?algorithm=SHA1&digits=6&issuer=auth.example.com&period=30&secret=5ZH7Y5CTFWOXN7EOLGBMMXADRNQFHVUDZSYKCN5HMFAIRSLAWY3Q
{{- end }}
{{- if .Webauthn }}
webauthn.PublicKeyCredential: webauthn.PublicKeyCredential:
type: object type: object
properties: properties:
@ -2129,6 +1880,8 @@ components:
written: written:
type: boolean type: boolean
example: false example: false
{{- end }}
{{- if .OpenIDConnect }}
openid.request.consent: openid.request.consent:
type: object type: object
properties: properties:
@ -3725,12 +3478,15 @@ components:
type: array type: array
items: items:
$ref: '#/components/schemas/jose.spec.JWK' $ref: '#/components/schemas/jose.spec.JWK'
{{- end }}
securitySchemes: securitySchemes:
authelia_auth: authelia_auth:
type: apiKey type: apiKey
name: "{{ .Session }}" name: "{{ .Session }}"
in: cookie in: cookie
{{- if .OpenIDConnect }}
openid: openid:
type: openIdConnect type: openIdConnect
openIdConnectUrl: "{{ .BaseURL }}.well-known/openid-configuration" openIdConnectUrl: "{{ .BaseURL }}.well-known/openid-configuration"
{{- end }}
... ...

View File

@ -1394,15 +1394,9 @@ notifier:
## Sets the client to public. This should typically not be set, please see the documentation for usage. ## Sets the client to public. This should typically not be set, please see the documentation for usage.
# public: false # public: false
## The policy to require for this client; one_factor or two_factor. ## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# authorization_policy: two_factor # redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
## Audience this client is allowed to request. ## Audience this client is allowed to request.
# audience: [] # audience: []
@ -1414,10 +1408,6 @@ notifier:
# - email # - email
# - profile # - profile
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Grant Types configures which grants this client can obtain. ## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing. ## It's not recommended to define this unless you know what you're doing.
# grant_types: # grant_types:
@ -1435,6 +1425,23 @@ notifier:
# - query # - query
# - fragment # - fragment
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## Enforces the use of PKCE for this client when set to true.
# enforce_pkce: false
## Enforces the use of PKCE for this client when configured, and enforces the specified challenge method.
## Options are 'plain' and 'S256'.
# pkce_challenge_method: S256
## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256. ## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256.
# userinfo_signing_algorithm: none # userinfo_signing_algorithm: none
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
... ...

View File

@ -404,12 +404,92 @@ useful for SPA's and CLI tools. This option requires setting the [client secret]
In addition to the standard rules for redirect URIs, public clients can use the `urn:ietf:wg:oauth:2.0:oob` redirect In addition to the standard rules for redirect URIs, public clients can use the `urn:ietf:wg:oauth:2.0:oob` redirect
URI. URI.
#### redirect_uris
{{< confkey type="list(string)" required="yes" >}}
A list of valid callback URIs this client will redirect to. All other callbacks will be considered unsafe. The URIs are
case-sensitive and they differ from application to application - the community has provided
[a list of URL´s for common applications](../../integration/openid-connect/introduction.md).
Some restrictions that have been placed on clients and
their redirect URIs are as follows:
1. If a client attempts to authorize with Authelia and its redirect URI is not listed in the client configuration the
attempt to authorize will fail and an error will be generated.
2. The redirect URIs are case-sensitive.
3. The URI must include a scheme and that scheme must be one of `http` or `https`.
4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
#### audience
{{< confkey type="list(string)" required="no" >}}
A list of audiences this client is allowed to request.
#### scopes
{{< confkey type="list(string)" default="openid, groups, profile, email" required="no" >}}
A list of scopes to allow this client to consume. See
[scope definitions](../../integration/openid-connect/introduction.md#scope-definitions) for more information. The
documentation for the application you want to use with Authelia will most-likely provide you with the scopes to allow.
#### grant_types
{{< confkey type="list(string)" default="refresh_token, authorization_code" required="no" >}}
A list of grant types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `implicit`, `refresh_token`, `authorization_code`, `password`,
`client_credentials`.
#### response_types
{{< confkey type="list(string)" default="code" required="no" >}}
A list of response types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `code`, `code id_token`, `id_token`, `token id_token`, `token`,
`token id_token code`.
#### response_modes
{{< confkey type="list(string)" default="form_post, query, fragment" required="no" >}}
A list of response modes this client can return. It is recommended that this isn't configured at this time unless you
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
#### authorization_policy #### authorization_policy
{{< confkey type="string" default="two_factor" required="no" >}} {{< confkey type="string" default="two_factor" 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`.
#### enforce_pkce
{{< confkey type="bool" default="false" required="no" >}}
This setting enforces the use of [PKCE] for this individual client. To enforce it for all clients see the global
[enforce_pkce](#enforcepkce) setting.
#### pkce_challenge_method
{{< confkey type="string" default="" required="no" >}}
This setting enforces the use of the specified [PKCE] challenge method for this individual client. This setting also
effectively enables the [enforce_pkce](#enforcepkce-1) option for this client.
Valid values are an empty string, `plain`, or `S256`. It should be noted that `S256` is strongly recommended if the
relying party supports it.
#### userinfo_signing_algorithm
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses. This can either be `none` or `RS256`.
See the [integration guide](../../integration/openid-connect/introduction.md#user-information-signing-algorithm) for
more information.
#### consent_mode #### consent_mode
{{< confkey type="string" default="auto" required="no" >}} {{< confkey type="string" default="auto" required="no" >}}
@ -442,69 +522,6 @@ match exactly with the granted scopes/audience.
[consent_mode]: #consentmode [consent_mode]: #consentmode
#### audience
{{< confkey type="list(string)" required="no" >}}
A list of audiences this client is allowed to request.
#### scopes
{{< confkey type="list(string)" default="openid, groups, profile, email" required="no" >}}
A list of scopes to allow this client to consume. See
[scope definitions](../../integration/openid-connect/introduction.md#scope-definitions) for more information. The
documentation for the application you want to use with Authelia will most-likely provide you with the scopes to allow.
#### redirect_uris
{{< confkey type="list(string)" required="yes" >}}
A list of valid callback URIs this client will redirect to. All other callbacks will be considered unsafe. The URIs are
case-sensitive and they differ from application to application - the community has provided
[a list of URL´s for common applications](../../integration/openid-connect/introduction.md).
Some restrictions that have been placed on clients and
their redirect URIs are as follows:
1. If a client attempts to authorize with Authelia and its redirect URI is not listed in the client configuration the
attempt to authorize will fail and an error will be generated.
2. The redirect URIs are case-sensitive.
3. The URI must include a scheme and that scheme must be one of `http` or `https`.
4. The client can ignore rule 3 and use `urn:ietf:wg:oauth:2.0:oob` if it is a [public](#public) client type.
#### grant_types
{{< confkey type="list(string)" default="refresh_token, authorization_code" required="no" >}}
A list of grant types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `implicit`, `refresh_token`, `authorization_code`, `password`,
`client_credentials`.
#### response_types
{{< confkey type="list(string)" default="code" required="no" >}}
A list of response types this client can return. *It is recommended that this isn't configured at this time unless you
know what you're doing*. Valid options are: `code`, `code id_token`, `id_token`, `token id_token`, `token`,
`token id_token code`.
#### response_modes
{{< confkey type="list(string)" default="form_post, query, fragment" required="no" >}}
A list of response modes this client can return. It is recommended that this isn't configured at this time unless you
know what you're doing. Potential values are `form_post`, `query`, and `fragment`.
#### userinfo_signing_algorithm
{{< confkey type="string" default="none" required="no" >}}
The algorithm used to sign the userinfo endpoint responses. This can either be `none` or `RS256`.
See the [integration guide](../../integration/openid-connect/introduction.md#user-information-signing-algorithm) for
more information.
## Integration ## Integration
To integrate Authelia's [OpenID Connect] implementation with a relying party please see the To integrate Authelia's [OpenID Connect] implementation with a relying party please see the

View File

@ -163,9 +163,9 @@ services:
In the [SWAG] `/config` mount which is mounted to `${PWD}/data/swag` in our example: In the [SWAG] `/config` mount which is mounted to `${PWD}/data/swag` in our example:
1. Create a folder named `snippets/authelia`: 1. Create a folder named `snippets/authelia`:
- The `mkdir -p ${PWD}/data/swag/snippets/authelia` command should achieve this on Linux. - The `mkdir -p ${PWD}/data/swag/nginx/snippets/authelia` command should achieve this on Linux.
2. Create the `${PWD}/data/swag/nginxsnippets/authelia/location.conf` file which can be found [here](nginx.md#authelia-locationconf). 2. Create the `${PWD}/data/swag/nginx/snippets/authelia/location.conf` file which can be found [here](nginx.md#authelia-locationconf).
3. Create the `${PWD}/data/swag/nginxsnippets/authelia/authrequest.conf` file which can be found [here](nginx.md#authelia-authrequestconf). 3. Create the `${PWD}/data/swag/nginx/snippets/authelia/authrequest.conf` file which can be found [here](nginx.md#authelia-authrequestconf).
- Ensure you adjust the line `error_page 401 =302 https://auth.example.com/?rd=$target_url;` replacing `https://auth.example.com/` with your external Authelia URL. - Ensure you adjust the line `error_page 401 =302 https://auth.example.com/?rd=$target_url;` replacing `https://auth.example.com/` with your external Authelia URL.
## Protected Application ## Protected Application
@ -174,7 +174,7 @@ In the server configuration for the application you want to protect:
1. Edit the `/config/nginx/proxy-confs/` file for the application you wish to protect. 1. Edit the `/config/nginx/proxy-confs/` file for the application you wish to protect.
2. Under the `#include /config/nginx/authelia-server.conf;` line which should be within the `server` block 2. Under the `#include /config/nginx/authelia-server.conf;` line which should be within the `server` block
but not inside any `location` blocks add the following line: ``. but not inside any `location` blocks add the following line: `include /config/nginx/snippets/authelia/location.conf;`.
3. Under the `#include /config/nginx/authelia-location.conf;` line which should be within the applications 3. Under the `#include /config/nginx/authelia-location.conf;` line which should be within the applications
`location` block add the following line `include /config/nginx/snippets/authelia/authrequest.conf;`. `location` block add the following line `include /config/nginx/snippets/authelia/authrequest.conf;`.

View File

@ -10,14 +10,31 @@ aliases:
--- ---
The __Authelia__ team aims to abide by the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) policy. This The __Authelia__ team aims to abide by the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) policy. This
means that we use the format `major.minor.patch` for our version numbers, where a change to `major` denotes a breaking means that we use the format `<major>.<minor>.<patch>` for our version numbers, where a change to `major` denotes a
change which will likely require user interaction to upgrade, `minor` which denotes a new feature, and `patch` denotes a breaking change which will likely require user interaction to upgrade, `minor` which denotes a new feature, and `patch`
fix. denotes a fix.
It is therefore recommended users do not automatically upgrade the `minor` version without reading the patch notes, and It is therefore recommended users do not automatically upgrade the `minor` version without reading the patch notes, and
it's critically important users do not upgrade the `major` version without reading the patch notes. You should pin your it's critically important users do not upgrade the `major` version without reading the patch notes. You should pin your
version to `4.37` for example to prevent automatic upgrades from negatively affecting you. version to `4.37` for example to prevent automatic upgrades of the `minor` version, or pin your version to `4` to
prevent automatic upgrade of the `major` version.
We generally do not recommend automated upgrades of critical systems but instead recommend ensuring you are notified an
upgrade exists.
## Major Version Zero
A major version of `v0.x.x` indicates as per the [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html) policy
that there may be breaking changes without warning. Some [components](#components) will be released under this version
while they're in early development.
## Components
Several components may exist at various times. We aim to abide by this policy for all components related to Authelia.
It is important to note that each component has its own version, for example the primary Authelia binary version may be
v4.40.0 but another component such as the [Helm Chart](https://charts.authelia.com) version may be v0.9.0.
This means that a breaking change may occur to one but not the other as these components do not share a version.
## Exceptions ## Exceptions
There are exceptions to this versioning policy. There are exceptions to this versioning policy.
@ -33,7 +50,7 @@ Notable Advanced Customizations:
- Templates: - Templates:
- Email - Email
- Content Security Policy header - Content Security Policy header
- Localization Assets - Localization / Internationalization Assets
### Breaking Changes ### Breaking Changes
@ -47,6 +64,6 @@ Notable examples:
- OpenID Connect 1.0 - OpenID Connect 1.0
- File Filters - File Filters
The reasoning is as we develop these features there may be mistakes and we may need to make a change that should be The reasoning is as we develop these features there may be mistakes and we may need to make a change that would normally
considered breaking. As these features graduate from their status to generally available they will move into our be considered a breaking change. As these features graduate from their status to generally available they will move into
standard versioning policy from this exception. our standard versioning policy and lose their exception status.

View File

@ -15,3 +15,5 @@ toc: true
## Web Portal Internationalization ## Web Portal Internationalization
{{% table-i18n-locales %}} {{% table-i18n-locales %}}
Information about overriding the web portal internationalization is available from the [Server Asset Overrides Reference Guide](./server-asset-overrides.md).

View File

@ -20,9 +20,9 @@ This guide effectively documents the usage of the
## Important Notes ## Important Notes
1. The templates are not covered by our stability guarantees. While we aim to avoid changes to the templates which 1. The templates are not covered by our stability guarantees as per our [Versioning Policy]. While we aim to avoid
would cause users to have to manually change them changes may be necessary in order to facilitate bug fixes or changes to the templates which would cause users to have to manually change them changes may be necessary in order to
generally improve the templates. facilitate bug fixes or generally improve the templates.
1. It is your responsibility to ensure your templates are up to date. We make no efforts in facilitating this. 1. It is your responsibility to ensure your templates are up to date. We make no efforts in facilitating this.
2. We may not be able to offer any direct support in debugging these templates. We only offer support and fixes to 2. We may not be able to offer any direct support in debugging these templates. We only offer support and fixes to
the official templates. the official templates.
@ -82,3 +82,4 @@ Several functions are implemented with the email templates. See the
[server_name]: ../../configuration/notifications/smtp.md#tls [server_name]: ../../configuration/notifications/smtp.md#tls
[sender]: ../../configuration/notifications/smtp.md#sender [sender]: ../../configuration/notifications/smtp.md#sender
[identifier]: ../../configuration/notifications/smtp.md#identifier [identifier]: ../../configuration/notifications/smtp.md#identifier
[Versioning Policy]: ../../policies/versioning.md

View File

@ -39,8 +39,8 @@ the language itself, or adding a variant form of that language. If you'd like su
to make a PR. We also encourage people to make PR's for variants where the difference in the variants is significant.* to make a PR. We also encourage people to make PR's for variants where the difference in the variants is significant.*
*__Important Note__ Users wishing to override the locales files should be aware that we do not provide any guarantee *__Important Note__ Users wishing to override the locales files should be aware that we do not provide any guarantee
that the file will not change in a breaking way between releases. Users who planning to utilize these that the file will not change in a breaking way between releases as per our [Versioning Policy]. Users who planning to
overrides should either check for changes to the files in the utilize these overrides should either check for changes to the files in the
[en](https://github.com/authelia/authelia/tree/master/internal/server/locales/en) translation prior to upgrading or [en](https://github.com/authelia/authelia/tree/master/internal/server/locales/en) translation prior to upgrading or
[Contribute](../../contributing/prologue/translations.md) their translation to ensure it is maintained.* [Contribute](../../contributing/prologue/translations.md) their translation to ensure it is maintained.*
@ -72,12 +72,8 @@ Each file in a locale directory represents a translation namespace. The list of
List of supported languages and variants: List of supported languages and variants:
| Description | Language | Additional Variants | Location | {{% table-i18n-overrides %}}
|:---------------------:|:--------:|:-------------------:|:--------------------:|
| English | en | N/A | locales/en/*.json | More information may be available from the [Internationalization Reference Guide](./internationalization.md).
| Spanish | es | N/A | locales/es/*.json |
| German | de | N/A | locales/de/*.json | [Versioning Policy]: ../../policies/versioning.md
| French | fr | N/A | locales/fr/*.json |
| Russian | ru | N/A | locales/ru/*.json |
| Swedish | sv | sv-SE (Sweden) | locales/sv/*.json |
| Chinese (Traditional) | zh-TW | N/A | locales/zh-TW/*.json |

View File

@ -2,7 +2,7 @@
title: "Templating" title: "Templating"
description: "A reference guide on the templates system" description: "A reference guide on the templates system"
lead: "This section contains reference documentation for Authelia's templating capabilities." lead: "This section contains reference documentation for Authelia's templating capabilities."
date: 2022-12-23T18:31:05+11:00 date: 2022-12-23T21:58:54+11:00
draft: false draft: false
images: [] images: []
menu: menu:
@ -56,6 +56,28 @@ The following functions which mimic the behaviour of helm exist in most templati
- b64dec - b64dec
- b32enc - b32enc
- b32dec - b32dec
- list
- dict
- get
- set
- isAbs
- base
- dir
- ext
- clean
- osBase
- osClean
- osDir
- osExt
- osIsAbs
- deepEqual
- typeOf
- typeIs
- typeIsLike
- kindOf
- kindIs
- default
- empty
See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please See the [Helm Documentation](https://helm.sh/docs/chart_template_guide/function_list/) for more information. Please
note that only the functions listed above are supported and the functions don't necessarily behave exactly the same. note that only the functions listed above are supported and the functions don't necessarily behave exactly the same.

View File

@ -64,7 +64,7 @@ Feature List:
Feature List: Feature List:
* [Proof Key Code Exchange (PKCE)](https://www.rfc-editor.org/rfc/rfc7636.html) for Authorization Code Flow * [Proof Key Code Exchange (PKCE)] for Authorization Code Flow
* Claims: * Claims:
* `preferred_username` - sending the username in this claim instead of the `sub` claim. * `preferred_username` - sending the username in this claim instead of the `sub` claim.
@ -115,8 +115,8 @@ Feature List:
{{< roadmap-status stage="in-progress" version="v4.38.0" >}} {{< roadmap-status stage="in-progress" version="v4.38.0" >}}
* [OAuth 2.0 Pushed Authorization Requests](https://www.rfc-editor.org/rfc/rfc9126.html) * [OAuth 2.0 Pushed Authorization Requests](https://www.rfc-editor.org/rfc/rfc9126.html)
* Per-Client [Proof Key Code Exchange (PKCE)] Policy
### Beta 7 ### Beta 7
@ -219,3 +219,4 @@ The `preferred_username` claim was missing and was fixed.
[OpenID Connect Core (Subject Identifier Types)]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes [OpenID Connect Core (Subject Identifier Types)]: https://openid.net/specs/openid-connect-core-1_0.html#SubjectIDTypes
[OpenID Connect Core (Pairwise Identifier Algorithm)]: https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg [OpenID Connect Core (Pairwise Identifier Algorithm)]: https://openid.net/specs/openid-connect-core-1_0.html#PairwiseAlg
[OpenID Connect Core (Mandatory to Implement Features for All OpenID Providers)]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI [OpenID Connect Core (Mandatory to Implement Features for All OpenID Providers)]: https://openid.net/specs/openid-connect-core-1_0.html#ServerMTI
[Proof Key Code Exchange (PKCE)]: https://www.rfc-editor.org/rfc/rfc7636.html

View File

@ -0,0 +1,5 @@
| Language | Locale | Override Path |
|:--------------:|:-------------:|:----------------------------:|
{{- range $.Site.Data.languages.languages }}
| {{ .display }} | {{ .locale }} | locales/{{ .locale }}/*.json |
{{- end }}

View File

@ -47,9 +47,9 @@
"auto-changelog": "2.4.0", "auto-changelog": "2.4.0",
"autoprefixer": "10.4.13", "autoprefixer": "10.4.13",
"bootstrap": "5.2.3", "bootstrap": "5.2.3",
"bootstrap-icons": "1.10.2", "bootstrap-icons": "1.10.3",
"clipboard": "2.0.11", "clipboard": "2.0.11",
"eslint": "8.30.0", "eslint": "8.31.0",
"exec-bin": "1.0.0", "exec-bin": "1.0.0",
"flexsearch": "0.7.31", "flexsearch": "0.7.31",
"highlight.js": "11.7.0", "highlight.js": "11.7.0",
@ -57,14 +57,14 @@
"instant.page": "5.1.1", "instant.page": "5.1.1",
"katex": "0.16.4", "katex": "0.16.4",
"lazysizes": "5.3.2", "lazysizes": "5.3.2",
"markdownlint-cli2": "0.5.1", "markdownlint-cli2": "0.6.0",
"netlify-plugin-submit-sitemap": "0.4.0", "netlify-plugin-submit-sitemap": "0.4.0",
"node-fetch": "3.3.0", "node-fetch": "3.3.0",
"postcss": "8.4.20", "postcss": "8.4.20",
"postcss-cli": "10.1.0", "postcss-cli": "10.1.0",
"purgecss-whitelister": "2.4.0", "purgecss-whitelister": "2.4.0",
"shx": "0.3.4", "shx": "0.3.4",
"stylelint": "14.16.0", "stylelint": "14.16.1",
"stylelint-config-standard-scss": "6.1.0" "stylelint-config-standard-scss": "6.1.0"
}, },
"otherDependencies": { "otherDependencies": {

View File

@ -10,9 +10,9 @@ specifiers:
auto-changelog: 2.4.0 auto-changelog: 2.4.0
autoprefixer: 10.4.13 autoprefixer: 10.4.13
bootstrap: 5.2.3 bootstrap: 5.2.3
bootstrap-icons: 1.10.2 bootstrap-icons: 1.10.3
clipboard: 2.0.11 clipboard: 2.0.11
eslint: 8.30.0 eslint: 8.31.0
exec-bin: 1.0.0 exec-bin: 1.0.0
flexsearch: 0.7.31 flexsearch: 0.7.31
highlight.js: 11.7.0 highlight.js: 11.7.0
@ -20,14 +20,14 @@ specifiers:
instant.page: 5.1.1 instant.page: 5.1.1
katex: 0.16.4 katex: 0.16.4
lazysizes: 5.3.2 lazysizes: 5.3.2
markdownlint-cli2: 0.5.1 markdownlint-cli2: 0.6.0
netlify-plugin-submit-sitemap: 0.4.0 netlify-plugin-submit-sitemap: 0.4.0
node-fetch: 3.3.0 node-fetch: 3.3.0
postcss: 8.4.20 postcss: 8.4.20
postcss-cli: 10.1.0 postcss-cli: 10.1.0
purgecss-whitelister: 2.4.0 purgecss-whitelister: 2.4.0
shx: 0.3.4 shx: 0.3.4
stylelint: 14.16.0 stylelint: 14.16.1
stylelint-config-standard-scss: 6.1.0 stylelint-config-standard-scss: 6.1.0
devDependencies: devDependencies:
@ -40,9 +40,9 @@ devDependencies:
auto-changelog: 2.4.0 auto-changelog: 2.4.0
autoprefixer: 10.4.13_postcss@8.4.20 autoprefixer: 10.4.13_postcss@8.4.20
bootstrap: 5.2.3_@popperjs+core@2.11.6 bootstrap: 5.2.3_@popperjs+core@2.11.6
bootstrap-icons: 1.10.2 bootstrap-icons: 1.10.3
clipboard: 2.0.11 clipboard: 2.0.11
eslint: 8.30.0 eslint: 8.31.0
exec-bin: 1.0.0 exec-bin: 1.0.0
flexsearch: 0.7.31 flexsearch: 0.7.31
highlight.js: 11.7.0 highlight.js: 11.7.0
@ -50,15 +50,15 @@ devDependencies:
instant.page: 5.1.1 instant.page: 5.1.1
katex: 0.16.4 katex: 0.16.4
lazysizes: 5.3.2 lazysizes: 5.3.2
markdownlint-cli2: 0.5.1 markdownlint-cli2: 0.6.0
netlify-plugin-submit-sitemap: 0.4.0 netlify-plugin-submit-sitemap: 0.4.0
node-fetch: 3.3.0 node-fetch: 3.3.0
postcss: 8.4.20 postcss: 8.4.20
postcss-cli: 10.1.0_postcss@8.4.20 postcss-cli: 10.1.0_postcss@8.4.20
purgecss-whitelister: 2.4.0 purgecss-whitelister: 2.4.0
shx: 0.3.4 shx: 0.3.4
stylelint: 14.16.0 stylelint: 14.16.1
stylelint-config-standard-scss: 6.1.0_l3s4qeu3gkar2knsdy3w5miimm stylelint-config-standard-scss: 6.1.0_vitr26fcqo6sphdfxyxll4n2gy
packages: packages:
@ -1331,8 +1331,8 @@ packages:
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.11
dev: true dev: true
/@eslint/eslintrc/1.4.0: /@eslint/eslintrc/1.4.1:
resolution: {integrity: sha512-7yfvXy6MWLgWSFsLhz5yH3iQ52St8cdUY6FoGieKkRDVxuxmrNuUetIuu6cmjNWwniUHiWXjxCr5tTXDrbYS5A==} resolution: {integrity: sha512-XXrH9Uarn0stsyldqDYq8r++mROmWRI1xKMXa640Bb//SY1+ECYX6VzT6Lcx5frD0V30XieqJ0oX9I2Xj5aoMA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies: dependencies:
ajv: 6.12.6 ajv: 6.12.6
@ -1683,8 +1683,8 @@ packages:
safe-buffer: 5.2.1 safe-buffer: 5.2.1
dev: true dev: true
/bootstrap-icons/1.10.2: /bootstrap-icons/1.10.3:
resolution: {integrity: sha512-PTPYadRn1AMGr+QTSxe4ZCc+Wzv9DGZxbi3lNse/dajqV31n2/wl/7NX78ZpkvFgRNmH4ogdIQPQmxAfhEV6nA==} resolution: {integrity: sha512-7Qvj0j0idEm/DdX9Q0CpxAnJYqBCFCiUI6qzSPYfERMcokVuV9Mdm/AJiVZI8+Gawe4h/l6zFcOzvV7oXCZArw==}
dev: true dev: true
/bootstrap/5.2.3_@popperjs+core@2.11.6: /bootstrap/5.2.3_@popperjs+core@2.11.6:
@ -2073,7 +2073,7 @@ packages:
resolution: {integrity: sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==} resolution: {integrity: sha512-tQbV/4u5WVB8HMJr08pgw0b6nG4RGt/tj+7Numvq+zqcvUFeMaIWWOUFltiU+6go8BSO2/ogsB4EasDaj0y68Q==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
dependencies: dependencies:
globby: 13.1.2 globby: 13.1.3
graceful-fs: 4.2.10 graceful-fs: 4.2.10
is-glob: 4.0.3 is-glob: 4.0.3
is-path-cwd: 3.0.0 is-path-cwd: 3.0.0
@ -2159,13 +2159,13 @@ packages:
estraverse: 5.3.0 estraverse: 5.3.0
dev: true dev: true
/eslint-utils/3.0.0_eslint@8.30.0: /eslint-utils/3.0.0_eslint@8.31.0:
resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==}
engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0}
peerDependencies: peerDependencies:
eslint: '>=5' eslint: '>=5'
dependencies: dependencies:
eslint: 8.30.0 eslint: 8.31.0
eslint-visitor-keys: 2.1.0 eslint-visitor-keys: 2.1.0
dev: true dev: true
@ -2179,12 +2179,12 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true dev: true
/eslint/8.30.0: /eslint/8.31.0:
resolution: {integrity: sha512-MGADB39QqYuzEGov+F/qb18r4i7DohCDOfatHaxI2iGlPuC65bwG2gxgO+7DkyL38dRFaRH7RaRAgU6JKL9rMQ==} resolution: {integrity: sha512-0tQQEVdmPZ1UtUKXjX7EMm9BlgJ08G90IhWh0PKDCb3ZLsgAOHI8fYSIzYVZej92zsgq+ft0FGsxhJ3xo2tbuA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
hasBin: true hasBin: true
dependencies: dependencies:
'@eslint/eslintrc': 1.4.0 '@eslint/eslintrc': 1.4.1
'@humanwhocodes/config-array': 0.11.8 '@humanwhocodes/config-array': 0.11.8
'@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/module-importer': 1.0.1
'@nodelib/fs.walk': 1.2.8 '@nodelib/fs.walk': 1.2.8
@ -2195,7 +2195,7 @@ packages:
doctrine: 3.0.0 doctrine: 3.0.0
escape-string-regexp: 4.0.0 escape-string-regexp: 4.0.0
eslint-scope: 7.1.1 eslint-scope: 7.1.1
eslint-utils: 3.0.0_eslint@8.30.0 eslint-utils: 3.0.0_eslint@8.31.0
eslint-visitor-keys: 3.3.0 eslint-visitor-keys: 3.3.0
espree: 9.4.0 espree: 9.4.0
esquery: 1.4.0 esquery: 1.4.0
@ -2553,6 +2553,17 @@ packages:
slash: 4.0.0 slash: 4.0.0
dev: true dev: true
/globby/13.1.3:
resolution: {integrity: sha512-8krCNHXvlCgHDpegPzleMq07yMYTO2sXKASmZmquEYWEmCx6J5UTRbp5RwMJkTJGtcQ44YpiUYUiN0b9mzy8Bw==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
dependencies:
dir-glob: 3.0.1
fast-glob: 3.2.12
ignore: 5.2.1
merge2: 1.4.1
slash: 4.0.0
dev: true
/globjoin/0.1.4: /globjoin/0.1.4:
resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==} resolution: {integrity: sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==}
dev: true dev: true
@ -3037,30 +3048,30 @@ packages:
uc.micro: 1.0.6 uc.micro: 1.0.6
dev: true dev: true
/markdownlint-cli2-formatter-default/0.0.3_markdownlint-cli2@0.5.1: /markdownlint-cli2-formatter-default/0.0.3_markdownlint-cli2@0.6.0:
resolution: {integrity: sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==} resolution: {integrity: sha512-QEAJitT5eqX1SNboOD+SO/LNBpu4P4je8JlR02ug2cLQAqmIhh8IJnSK7AcaHBHhNADqdGydnPpQOpsNcEEqCw==}
peerDependencies: peerDependencies:
markdownlint-cli2: '>=0.0.4' markdownlint-cli2: '>=0.0.4'
dependencies: dependencies:
markdownlint-cli2: 0.5.1 markdownlint-cli2: 0.6.0
dev: true dev: true
/markdownlint-cli2/0.5.1: /markdownlint-cli2/0.6.0:
resolution: {integrity: sha512-f3Nb1GF/c8YSrV/FntsCWzpa5mLFJRlO+wzEgv+lkNQjU6MZflUwc2FbyEDPTo6oVhP2VyUOkK0GkFgfuktl1w==} resolution: {integrity: sha512-Bv20r6WGdcHMWi8QvAFZ3CBunf4i4aYmVdTfpAvXODI/1k3f09DZZ0i0LcX9ZMhlVxjoOzbVDz1NWyKc5hwTqg==}
engines: {node: '>=14'} engines: {node: '>=14.18.0'}
hasBin: true hasBin: true
dependencies: dependencies:
globby: 13.1.2 globby: 13.1.3
markdownlint: 0.26.2 markdownlint: 0.27.0
markdownlint-cli2-formatter-default: 0.0.3_markdownlint-cli2@0.5.1 markdownlint-cli2-formatter-default: 0.0.3_markdownlint-cli2@0.6.0
micromatch: 4.0.5 micromatch: 4.0.5
strip-json-comments: 5.0.0 strip-json-comments: 5.0.0
yaml: 2.1.1 yaml: 2.2.1
dev: true dev: true
/markdownlint/0.26.2: /markdownlint/0.27.0:
resolution: {integrity: sha512-2Am42YX2Ex5SQhRq35HxYWDfz1NLEOZWWN25nqd2h3AHRKsGRE+Qg1gt1++exW792eXTrR4jCNHfShfWk9Nz8w==} resolution: {integrity: sha512-HtfVr/hzJJmE0C198F99JLaeada+646B5SaG2pVoEakLFI6iRGsvMqrnnrflq8hm1zQgwskEgqSnhDW11JBp0w==}
engines: {node: '>=14'} engines: {node: '>=14.18.0'}
dependencies: dependencies:
markdown-it: 13.0.1 markdown-it: 13.0.1
dev: true dev: true
@ -3431,7 +3442,7 @@ packages:
dependencies: dependencies:
lilconfig: 2.0.5 lilconfig: 2.0.5
postcss: 8.4.20 postcss: 8.4.20
yaml: 2.1.1 yaml: 2.2.1
dev: true dev: true
/postcss-media-query-parser/0.2.3: /postcss-media-query-parser/0.2.3:
@ -3928,7 +3939,7 @@ packages:
resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==} resolution: {integrity: sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==}
dev: true dev: true
/stylelint-config-recommended-scss/8.0.0_l3s4qeu3gkar2knsdy3w5miimm: /stylelint-config-recommended-scss/8.0.0_vitr26fcqo6sphdfxyxll4n2gy:
resolution: {integrity: sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ==} resolution: {integrity: sha512-BxjxEzRaZoQb7Iinc3p92GS6zRdRAkIuEu2ZFLTxJK2e1AIcCb5B5MXY9KOXdGTnYFZ+KKx6R4Fv9zU6CtMYPQ==}
peerDependencies: peerDependencies:
postcss: ^8.3.3 postcss: ^8.3.3
@ -3939,20 +3950,20 @@ packages:
dependencies: dependencies:
postcss: 8.4.20 postcss: 8.4.20
postcss-scss: 4.0.4_postcss@8.4.20 postcss-scss: 4.0.4_postcss@8.4.20
stylelint: 14.16.0 stylelint: 14.16.1
stylelint-config-recommended: 9.0.0_stylelint@14.16.0 stylelint-config-recommended: 9.0.0_stylelint@14.16.1
stylelint-scss: 4.2.0_stylelint@14.16.0 stylelint-scss: 4.2.0_stylelint@14.16.1
dev: true dev: true
/stylelint-config-recommended/9.0.0_stylelint@14.16.0: /stylelint-config-recommended/9.0.0_stylelint@14.16.1:
resolution: {integrity: sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==} resolution: {integrity: sha512-9YQSrJq4NvvRuTbzDsWX3rrFOzOlYBmZP+o513BJN/yfEmGSr0AxdvrWs0P/ilSpVV/wisamAHu5XSk8Rcf4CQ==}
peerDependencies: peerDependencies:
stylelint: ^14.10.0 stylelint: ^14.10.0
dependencies: dependencies:
stylelint: 14.16.0 stylelint: 14.16.1
dev: true dev: true
/stylelint-config-standard-scss/6.1.0_l3s4qeu3gkar2knsdy3w5miimm: /stylelint-config-standard-scss/6.1.0_vitr26fcqo6sphdfxyxll4n2gy:
resolution: {integrity: sha512-iZ2B5kQT2G3rUzx+437cEpdcnFOQkwnwqXuY8Z0QUwIHQVE8mnYChGAquyKFUKZRZ0pRnrciARlPaR1RBtPb0Q==} resolution: {integrity: sha512-iZ2B5kQT2G3rUzx+437cEpdcnFOQkwnwqXuY8Z0QUwIHQVE8mnYChGAquyKFUKZRZ0pRnrciARlPaR1RBtPb0Q==}
peerDependencies: peerDependencies:
postcss: ^8.3.3 postcss: ^8.3.3
@ -3962,21 +3973,21 @@ packages:
optional: true optional: true
dependencies: dependencies:
postcss: 8.4.20 postcss: 8.4.20
stylelint: 14.16.0 stylelint: 14.16.1
stylelint-config-recommended-scss: 8.0.0_l3s4qeu3gkar2knsdy3w5miimm stylelint-config-recommended-scss: 8.0.0_vitr26fcqo6sphdfxyxll4n2gy
stylelint-config-standard: 29.0.0_stylelint@14.16.0 stylelint-config-standard: 29.0.0_stylelint@14.16.1
dev: true dev: true
/stylelint-config-standard/29.0.0_stylelint@14.16.0: /stylelint-config-standard/29.0.0_stylelint@14.16.1:
resolution: {integrity: sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg==} resolution: {integrity: sha512-uy8tZLbfq6ZrXy4JKu3W+7lYLgRQBxYTUUB88vPgQ+ZzAxdrvcaSUW9hOMNLYBnwH+9Kkj19M2DHdZ4gKwI7tg==}
peerDependencies: peerDependencies:
stylelint: ^14.14.0 stylelint: ^14.14.0
dependencies: dependencies:
stylelint: 14.16.0 stylelint: 14.16.1
stylelint-config-recommended: 9.0.0_stylelint@14.16.0 stylelint-config-recommended: 9.0.0_stylelint@14.16.1
dev: true dev: true
/stylelint-scss/4.2.0_stylelint@14.16.0: /stylelint-scss/4.2.0_stylelint@14.16.1:
resolution: {integrity: sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==} resolution: {integrity: sha512-HHHMVKJJ5RM9pPIbgJ/XA67h9H0407G68Rm69H4fzFbFkyDMcTV1Byep3qdze5+fJ3c0U7mJrbj6S0Fg072uZA==}
peerDependencies: peerDependencies:
stylelint: ^14.5.1 stylelint: ^14.5.1
@ -3986,11 +3997,11 @@ packages:
postcss-resolve-nested-selector: 0.1.1 postcss-resolve-nested-selector: 0.1.1
postcss-selector-parser: 6.0.11 postcss-selector-parser: 6.0.11
postcss-value-parser: 4.2.0 postcss-value-parser: 4.2.0
stylelint: 14.16.0 stylelint: 14.16.1
dev: true dev: true
/stylelint/14.16.0: /stylelint/14.16.1:
resolution: {integrity: sha512-X6uTi9DcxjzLV8ZUAjit1vsRtSwcls0nl07c9rqOPzvpA8IvTX/xWEkBRowS0ffevRrqkHa/ThDEu86u73FQDg==} resolution: {integrity: sha512-ErlzR/T3hhbV+a925/gbfc3f3Fep9/bnspMiJPorfGEmcBbXdS+oo6LrVtoUZ/w9fqD6o6k7PtUlCOsCRdjX/A==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
hasBin: true hasBin: true
dependencies: dependencies:
@ -4339,8 +4350,8 @@ packages:
engines: {node: '>= 6'} engines: {node: '>= 6'}
dev: true dev: true
/yaml/2.1.1: /yaml/2.2.1:
resolution: {integrity: sha512-o96x3OPo8GjWeSLF+wOAbrPfhFOGY0W00GNaxCDv+9hkcDJEnev1yh8S7pgHF0ik6zc8sQLuL8hjHjJULZp8bw==} resolution: {integrity: sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==}
engines: {node: '>= 14'} engines: {node: '>= 14'}
dev: true dev: true

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 236 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 62 KiB

18
go.mod
View File

@ -5,7 +5,7 @@ go 1.19
require ( require (
github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4 github.com/Gurpartap/logrus-stack v0.0.0-20170710170904-89c00d8a28f4
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
github.com/deckarep/golang-set v1.8.0 github.com/deckarep/golang-set/v2 v2.1.0
github.com/duosecurity/duo_api_golang v0.0.0-20221117185402-091daa09e19d github.com/duosecurity/duo_api_golang v0.0.0-20221117185402-091daa09e19d
github.com/fasthttp/router v1.4.14 github.com/fasthttp/router v1.4.14
github.com/fasthttp/session/v2 v2.4.13 github.com/fasthttp/session/v2 v2.4.13
@ -19,16 +19,16 @@ require (
github.com/golang-jwt/jwt/v4 v4.4.3 github.com/golang-jwt/jwt/v4 v4.4.3
github.com/golang/mock v1.6.0 github.com/golang/mock v1.6.0
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
github.com/hashicorp/go-retryablehttp v0.7.1 github.com/hashicorp/go-retryablehttp v0.7.2
github.com/jackc/pgx/v5 v5.2.0 github.com/jackc/pgx/v5 v5.2.0
github.com/jmoiron/sqlx v1.3.5 github.com/jmoiron/sqlx v1.3.5
github.com/knadh/koanf v1.4.4 github.com/knadh/koanf v1.4.5
github.com/mattn/go-sqlite3 v1.14.16 github.com/mattn/go-sqlite3 v1.14.16
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826
github.com/ory/fosite v0.44.0 github.com/ory/fosite v0.44.0
github.com/ory/herodot v0.9.13 github.com/ory/herodot v0.9.13
github.com/ory/x v0.0.523 github.com/ory/x v0.0.528
github.com/otiai10/copy v1.9.0 github.com/otiai10/copy v1.9.0
github.com/pkg/errors v0.9.1 github.com/pkg/errors v0.9.1
github.com/pquerna/otp v1.4.0 github.com/pquerna/otp v1.4.0
@ -39,11 +39,10 @@ require (
github.com/stretchr/testify v1.8.1 github.com/stretchr/testify v1.8.1
github.com/trustelem/zxcvbn v1.0.1 github.com/trustelem/zxcvbn v1.0.1
github.com/valyala/fasthttp v1.43.0 github.com/valyala/fasthttp v1.43.0
github.com/wneessen/go-mail v0.3.6 github.com/wneessen/go-mail v0.3.7
golang.org/x/net v0.4.0
golang.org/x/sync v0.1.0 golang.org/x/sync v0.1.0
golang.org/x/term v0.3.0 golang.org/x/term v0.4.0
golang.org/x/text v0.5.0 golang.org/x/text v0.6.0
gopkg.in/square/go-jose.v2 v2.6.0 gopkg.in/square/go-jose.v2 v2.6.0
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@ -110,8 +109,9 @@ require (
github.com/ysmood/leakless v0.8.0 // indirect github.com/ysmood/leakless v0.8.0 // indirect
golang.org/x/crypto v0.1.0 // indirect golang.org/x/crypto v0.1.0 // indirect
golang.org/x/mod v0.6.0 // indirect golang.org/x/mod v0.6.0 // indirect
golang.org/x/net v0.5.0 // indirect
golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b // indirect
golang.org/x/sys v0.3.0 // indirect golang.org/x/sys v0.4.0 // indirect
golang.org/x/tools v0.2.0 // indirect golang.org/x/tools v0.2.0 // indirect
google.golang.org/appengine v1.6.7 // indirect google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71 // indirect

36
go.sum
View File

@ -113,8 +113,8 @@ github.com/dave/jennifer v1.6.0/go.mod h1:AxTG893FiZKqxy3FP1kL80VMshSMuz2G+Egvsz
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/deckarep/golang-set v1.8.0 h1:sk9/l/KqpunDwP7pSjUg0keiOOLEnOBHzykLrsPppp4= github.com/deckarep/golang-set/v2 v2.1.0 h1:g47V4Or+DUdzbs8FxCCmgb6VYd+ptPAngjM6dtGktsI=
github.com/deckarep/golang-set v1.8.0/go.mod h1:5nI87KwE7wgsBU1F4GKAw2Qod7p5kyS383rP6+o6qqo= github.com/deckarep/golang-set/v2 v2.1.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4=
github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE= github.com/dgraph-io/ristretto v0.0.1/go.mod h1:T40EBc7CJke8TkpiYfGGKAeFjSaxuFXhuXRyumBd6RE=
github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E= github.com/dgraph-io/ristretto v0.0.2/go.mod h1:KPxhHT9ZxKefz+PCeOGsrHpl1qZ7i70dGTu2u+Ahh6E=
github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8=
@ -305,8 +305,8 @@ github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHh
github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA=
github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY= github.com/hashicorp/go-plugin v1.0.1/go.mod h1:++UyYGoz3o5w9ZzAdZxtQKrWWP+iqPBn3cQptSMzBuY=
github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= github.com/hashicorp/go-retryablehttp v0.5.4/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs=
github.com/hashicorp/go-retryablehttp v0.7.1 h1:sUiuQAnLlbvmExtFQs72iFW/HXeUn8Z1aJLQ4LJJbTQ= github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUDDYFRKq/RAd0=
github.com/hashicorp/go-retryablehttp v0.7.1/go.mod h1:vAew36LZh98gCBJNLH42IQ1ER/9wtLZZ8meHqQvEYWY= github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8=
github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.1/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
@ -366,8 +366,8 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk=
github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY= github.com/klauspost/compress v1.15.9 h1:wKRjX6JRtDdrE9qwa4b/Cip7ACOshUI4smpCQanqjSY=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
github.com/knadh/koanf v1.4.4 h1:d2jY5nCCeoaiqvEKSBW9rEc93EfNy/XWgWsSB3j7JEA= github.com/knadh/koanf v1.4.5 h1:yKWFswTrqFc0u7jBAoERUz30+N1b1yPXU01gAPr8IrY=
github.com/knadh/koanf v1.4.4/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs= github.com/knadh/koanf v1.4.5/go.mod h1:Hgyjp4y8v44hpZtPzs7JZfRAW5AhN7KfZcwv1RYggDs=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
@ -461,8 +461,8 @@ github.com/ory/herodot v0.9.13 h1:cN/Z4eOkErl/9W7hDIDLb79IO/bfsH+8yscBjRpB4IU=
github.com/ory/herodot v0.9.13/go.mod h1:IWDs9kSvFQqw/cQ8zi5ksyYvITiUU4dI7glUrhZcJYo= github.com/ory/herodot v0.9.13/go.mod h1:IWDs9kSvFQqw/cQ8zi5ksyYvITiUU4dI7glUrhZcJYo=
github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE= github.com/ory/viper v1.7.5 h1:+xVdq7SU3e1vNaCsk/ixsfxE4zylk1TJUiJrY647jUE=
github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM= github.com/ory/viper v1.7.5/go.mod h1:ypOuyJmEUb3oENywQZRgeAMwqgOyDqwboO1tj3DjTaM=
github.com/ory/x v0.0.523 h1:vn8e+8tV3RqD8RlvoE6lLPUnjpjua1ExJDMFy3Z5TAQ= github.com/ory/x v0.0.528 h1:26fXxJ5Zl5XDFjiCt5jdWQOI89Q2XogB1EnUeqx7P+M=
github.com/ory/x v0.0.523/go.mod h1:ayJio5x/fK4RwTgfgzs3JetOaaOSxso9hQjc3mFY8z0= github.com/ory/x v0.0.528/go.mod h1:XBqhPZRppPHTxtsE0l0oI/B2Onf1QJtMRGPh3CpEpA0=
github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4= github.com/otiai10/copy v1.9.0 h1:7KFNiCgZ91Ru4qW4CWPf/7jqtxLagGRmIxWldPP9VY4=
github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI= github.com/otiai10/copy v1.9.0/go.mod h1:hsfX19wcn0UWIHUQ3/4fHuehhk2UyArQ9dVFAn3FczI=
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
@ -609,8 +609,8 @@ github.com/valyala/fasthttp v1.42.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seB
github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g= github.com/valyala/fasthttp v1.43.0 h1:Gy4sb32C98fbzVWZlTM1oTMdLWGyvxR03VhM6cBIU4g=
github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY= github.com/valyala/fasthttp v1.43.0/go.mod h1:f6VbjjoI3z1NDOZOv17o6RvtRSWxC77seBFc2uWtgiY=
github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc= github.com/valyala/tcplisten v1.0.0/go.mod h1:T0xQ8SeCZGxckz9qRXTfG43PvQ/mcWh7FwZEA7Ioqkc=
github.com/wneessen/go-mail v0.3.6 h1:hT8PMIBdcTkoiDwoUGJssPYOe1Gg1/cUcp2o9+ls63o= github.com/wneessen/go-mail v0.3.7 h1:loEAGLvsDZLSiE6c+keBfg0gpias/R3ocFU8Eoh3Pq4=
github.com/wneessen/go-mail v0.3.6/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E= github.com/wneessen/go-mail v0.3.7/go.mod h1:m25lkU2GYQnlVr6tdwK533/UXxo57V0kLOjaFYmub0E=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
@ -747,8 +747,8 @@ golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk=
golang.org/x/net v0.4.0 h1:Q5QPcMlvfxFTAPV0+07Xz/MpK9NTXu2VDUuy0FeMfaU= golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@ -849,12 +849,12 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.3.0 h1:qoo4akIqOcDME5bhc/NgxUdovd6BSS2uMsVjB56q1xI= golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg=
golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -865,8 +865,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=

View File

@ -1,6 +1,7 @@
package commands package commands
import ( import (
"context"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
"os" "os"
@ -8,7 +9,6 @@ import (
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
"golang.org/x/net/context"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
@ -22,6 +22,7 @@ import (
"github.com/authelia/authelia/v4/internal/notification" "github.com/authelia/authelia/v4/internal/notification"
"github.com/authelia/authelia/v4/internal/ntp" "github.com/authelia/authelia/v4/internal/ntp"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
@ -43,6 +44,9 @@ func NewCmdCtx() *CmdCtx {
cancel: cancel, cancel: cancel,
group: group, group: group,
log: logging.Logger(), log: logging.Logger(),
providers: middlewares.Providers{
Random: &random.Cryptographical{},
},
config: &schema.Configuration{}, config: &schema.Configuration{},
} }
} }
@ -139,48 +143,43 @@ func (ctx *CmdCtx) LoadProviders() (warns, errs []error) {
return warns, errs return warns, errs
} }
storage := getStorageProvider(ctx) ctx.providers.StorageProvider = getStorageProvider(ctx)
providers := middlewares.Providers{ ctx.providers.Authorizer = authorization.NewAuthorizer(ctx.config)
Authorizer: authorization.NewAuthorizer(ctx.config), ctx.providers.NTP = ntp.NewProvider(&ctx.config.NTP)
NTP: ntp.NewProvider(&ctx.config.NTP), ctx.providers.PasswordPolicy = middlewares.NewPasswordPolicyProvider(ctx.config.PasswordPolicy)
PasswordPolicy: middlewares.NewPasswordPolicyProvider(ctx.config.PasswordPolicy), ctx.providers.Regulator = regulation.NewRegulator(ctx.config.Regulation, ctx.providers.StorageProvider, utils.RealClock{})
Regulator: regulation.NewRegulator(ctx.config.Regulation, storage, utils.RealClock{}), ctx.providers.SessionProvider = session.NewProvider(ctx.config.Session, ctx.trusted)
SessionProvider: session.NewProvider(ctx.config.Session, ctx.trusted), ctx.providers.TOTP = totp.NewTimeBasedProvider(ctx.config.TOTP)
StorageProvider: storage,
TOTP: totp.NewTimeBasedProvider(ctx.config.TOTP),
}
var err error var err error
switch { switch {
case ctx.config.AuthenticationBackend.File != nil: case ctx.config.AuthenticationBackend.File != nil:
providers.UserProvider = authentication.NewFileUserProvider(ctx.config.AuthenticationBackend.File) ctx.providers.UserProvider = authentication.NewFileUserProvider(ctx.config.AuthenticationBackend.File)
case ctx.config.AuthenticationBackend.LDAP != nil: case ctx.config.AuthenticationBackend.LDAP != nil:
providers.UserProvider = authentication.NewLDAPUserProvider(ctx.config.AuthenticationBackend, ctx.trusted) ctx.providers.UserProvider = authentication.NewLDAPUserProvider(ctx.config.AuthenticationBackend, ctx.trusted)
} }
if providers.Templates, err = templates.New(templates.Config{EmailTemplatesPath: ctx.config.Notifier.TemplatePath}); err != nil { if ctx.providers.Templates, err = templates.New(templates.Config{EmailTemplatesPath: ctx.config.Notifier.TemplatePath}); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
switch { switch {
case ctx.config.Notifier.SMTP != nil: case ctx.config.Notifier.SMTP != nil:
providers.Notifier = notification.NewSMTPNotifier(ctx.config.Notifier.SMTP, ctx.trusted) ctx.providers.Notifier = notification.NewSMTPNotifier(ctx.config.Notifier.SMTP, ctx.trusted)
case ctx.config.Notifier.FileSystem != nil: case ctx.config.Notifier.FileSystem != nil:
providers.Notifier = notification.NewFileNotifier(*ctx.config.Notifier.FileSystem) ctx.providers.Notifier = notification.NewFileNotifier(*ctx.config.Notifier.FileSystem)
} }
if providers.OpenIDConnect, err = oidc.NewOpenIDConnectProvider(ctx.config.IdentityProviders.OIDC, storage); err != nil { if ctx.providers.OpenIDConnect, err = oidc.NewOpenIDConnectProvider(ctx.config.IdentityProviders.OIDC, ctx.providers.StorageProvider); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
if ctx.config.Telemetry.Metrics.Enabled { if ctx.config.Telemetry.Metrics.Enabled {
providers.Metrics = metrics.NewPrometheus() ctx.providers.Metrics = metrics.NewPrometheus()
} }
ctx.providers = providers
return warns, errs return warns, errs
} }

View File

@ -3,7 +3,6 @@ package commands
import ( import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"fmt" "fmt"
@ -262,7 +261,7 @@ func (ctx *CmdCtx) CryptoGenerateRunE(cmd *cobra.Command, args []string) (err er
privateKey any privateKey any
) )
if privateKey, err = cryptoGenPrivateKeyFromCmd(cmd); err != nil { if privateKey, err = ctx.cryptoGenPrivateKeyFromCmd(cmd); err != nil {
return err return err
} }
@ -279,7 +278,7 @@ func (ctx *CmdCtx) CryptoCertificateRequestRunE(cmd *cobra.Command, _ []string)
privateKey any privateKey any
) )
if privateKey, err = cryptoGenPrivateKeyFromCmd(cmd); err != nil { if privateKey, err = ctx.cryptoGenPrivateKeyFromCmd(cmd); err != nil {
return err return err
} }
@ -326,7 +325,7 @@ func (ctx *CmdCtx) CryptoCertificateRequestRunE(cmd *cobra.Command, _ []string)
b.Reset() b.Reset()
if csr, err = x509.CreateCertificateRequest(rand.Reader, template, privateKey); err != nil { if csr, err = x509.CreateCertificateRequest(ctx.providers.Random, template, privateKey); err != nil {
return fmt.Errorf("failed to create certificate request: %w", err) return fmt.Errorf("failed to create certificate request: %w", err)
} }
@ -366,7 +365,7 @@ func (ctx *CmdCtx) CryptoCertificateGenerateRunE(cmd *cobra.Command, _ []string,
signatureKey = caPrivateKey signatureKey = caPrivateKey
} }
if template, err = cryptoGetCertificateFromCmd(cmd); err != nil { if template, err = ctx.cryptoGetCertificateFromCmd(cmd); err != nil {
return err return err
} }
@ -423,7 +422,7 @@ func (ctx *CmdCtx) CryptoCertificateGenerateRunE(cmd *cobra.Command, _ []string,
b.Reset() b.Reset()
if certificate, err = x509.CreateCertificate(rand.Reader, template, parent, publicKey, signatureKey); err != nil { if certificate, err = x509.CreateCertificate(ctx.providers.Random, template, parent, publicKey, signatureKey); err != nil {
return fmt.Errorf("failed to create certificate: %w", err) return fmt.Errorf("failed to create certificate: %w", err)
} }

View File

@ -4,7 +4,6 @@ import (
"crypto/ecdsa" "crypto/ecdsa"
"crypto/ed25519" "crypto/ed25519"
"crypto/elliptic" "crypto/elliptic"
"crypto/rand"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"crypto/x509/pkix" "crypto/x509/pkix"
@ -130,7 +129,7 @@ func cryptoGetWritePathsFromCmd(cmd *cobra.Command) (privateKey, publicKey strin
return filepath.Join(dir, private), filepath.Join(dir, public), nil return filepath.Join(dir, private), filepath.Join(dir, public), nil
} }
func cryptoGenPrivateKeyFromCmd(cmd *cobra.Command) (privateKey any, err error) { func (ctx *CmdCtx) cryptoGenPrivateKeyFromCmd(cmd *cobra.Command) (privateKey any, err error) {
switch cmd.Parent().Use { switch cmd.Parent().Use {
case cmdUseRSA: case cmdUseRSA:
var ( var (
@ -141,7 +140,7 @@ func cryptoGenPrivateKeyFromCmd(cmd *cobra.Command) (privateKey any, err error)
return nil, err return nil, err
} }
if privateKey, err = rsa.GenerateKey(rand.Reader, bits); err != nil { if privateKey, err = rsa.GenerateKey(ctx.providers.Random, bits); err != nil {
return nil, fmt.Errorf("generating RSA private key resulted in an error: %w", err) return nil, fmt.Errorf("generating RSA private key resulted in an error: %w", err)
} }
case cmdUseECDSA: case cmdUseECDSA:
@ -158,11 +157,11 @@ func cryptoGenPrivateKeyFromCmd(cmd *cobra.Command) (privateKey any, err error)
return nil, fmt.Errorf("invalid curve '%s' was specified: curve must be P224, P256, P384, or P521", curveStr) return nil, fmt.Errorf("invalid curve '%s' was specified: curve must be P224, P256, P384, or P521", curveStr)
} }
if privateKey, err = ecdsa.GenerateKey(curve, rand.Reader); err != nil { if privateKey, err = ecdsa.GenerateKey(curve, ctx.providers.Random); err != nil {
return nil, fmt.Errorf("generating ECDSA private key resulted in an error: %w", err) return nil, fmt.Errorf("generating ECDSA private key resulted in an error: %w", err)
} }
case cmdUseEd25519: case cmdUseEd25519:
if _, privateKey, err = ed25519.GenerateKey(rand.Reader); err != nil { if _, privateKey, err = ed25519.GenerateKey(ctx.providers.Random); err != nil {
return nil, fmt.Errorf("generating Ed25519 private key resulted in an error: %w", err) return nil, fmt.Errorf("generating Ed25519 private key resulted in an error: %w", err)
} }
} }
@ -336,7 +335,7 @@ func cryptoGetSubjectFromCmd(cmd *cobra.Command) (subject *pkix.Name, err error)
}, nil }, nil
} }
func cryptoGetCertificateFromCmd(cmd *cobra.Command) (certificate *x509.Certificate, err error) { func (ctx *CmdCtx) cryptoGetCertificateFromCmd(cmd *cobra.Command) (certificate *x509.Certificate, err error) {
var ( var (
ca bool ca bool
subject *pkix.Name subject *pkix.Name
@ -378,7 +377,7 @@ func cryptoGetCertificateFromCmd(cmd *cobra.Command) (certificate *x509.Certific
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128) serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
if serialNumber, err = rand.Int(rand.Reader, serialNumberLimit); err != nil { if serialNumber, err = ctx.providers.Random.IntErr(serialNumberLimit); err != nil {
return nil, fmt.Errorf("failed to generate serial number: %w", err) return nil, fmt.Errorf("failed to generate serial number: %w", err)
} }

View File

@ -122,6 +122,8 @@ func runServices(ctx *CmdCtx) {
}() }()
if mainServer, mainListener, err = server.CreateDefaultServer(*ctx.config, ctx.providers); err != nil { if mainServer, mainListener, err = server.CreateDefaultServer(*ctx.config, ctx.providers); err != nil {
ctx.log.WithError(err).Error("Create Server (main) returned error")
return err return err
} }
@ -146,6 +148,8 @@ func runServices(ctx *CmdCtx) {
}() }()
if metricsServer, metricsListener, err = server.CreateMetricsServer(ctx.config.Telemetry.Metrics); err != nil { if metricsServer, metricsListener, err = server.CreateMetricsServer(ctx.config.Telemetry.Metrics); err != nil {
ctx.log.WithError(err).Error("Create Server (metrics) returned error")
return err return err
} }
@ -163,7 +167,11 @@ func runServices(ctx *CmdCtx) {
if watcher, err := runServiceFileWatcher(ctx, ctx.config.AuthenticationBackend.File.Path, provider); err != nil { if watcher, err := runServiceFileWatcher(ctx, ctx.config.AuthenticationBackend.File.Path, provider); err != nil {
ctx.log.WithError(err).Errorf("Error opening file watcher") ctx.log.WithError(err).Errorf("Error opening file watcher")
} else { } else {
defer watcher.Close() defer func(watcher *fsnotify.Watcher) {
if err := watcher.Close(); err != nil {
ctx.log.WithError(err).Errorf("Error closing file watcher")
}
}(watcher)
} }
} }

View File

@ -18,6 +18,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/validator" "github.com/authelia/authelia/v4/internal/configuration/validator"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
"github.com/authelia/authelia/v4/internal/totp" "github.com/authelia/authelia/v4/internal/totp"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
@ -983,7 +984,8 @@ func (ctx *CmdCtx) StorageUserTOTPExportPNGRunE(cmd *cobra.Command, _ []string)
} }
if dir == "" { if dir == "" {
dir = utils.RandomString(8, utils.CharSetAlphaNumeric) rand := &random.Cryptographical{}
dir = rand.StringCustom(8, random.CharSetAlphaNumeric)
} }
if _, err = os.Stat(dir); !os.IsNotExist(err) { if _, err = os.Stat(dir); !os.IsNotExist(err) {

View File

@ -14,7 +14,7 @@ import (
"golang.org/x/term" "golang.org/x/term"
"github.com/authelia/authelia/v4/internal/configuration" "github.com/authelia/authelia/v4/internal/configuration"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/random"
) )
func recoverErr(i any) error { func recoverErr(i any) error {
@ -77,29 +77,29 @@ func flagsGetRandomCharacters(flags *pflag.FlagSet, flagNameLength, flagNameChar
switch c { switch c {
case "ascii": case "ascii":
charset = utils.CharSetASCII charset = random.CharSetASCII
case "alphanumeric": case "alphanumeric":
charset = utils.CharSetAlphaNumeric charset = random.CharSetAlphaNumeric
case "alphanumeric-lower": case "alphanumeric-lower":
charset = utils.CharSetAlphabeticLower + utils.CharSetNumeric charset = random.CharSetAlphabeticLower + random.CharSetNumeric
case "alphanumeric-upper": case "alphanumeric-upper":
charset = utils.CharSetAlphabeticUpper + utils.CharSetNumeric charset = random.CharSetAlphabeticUpper + random.CharSetNumeric
case "alphabetic": case "alphabetic":
charset = utils.CharSetAlphabetic charset = random.CharSetAlphabetic
case "alphabetic-lower": case "alphabetic-lower":
charset = utils.CharSetAlphabeticLower charset = random.CharSetAlphabeticLower
case "alphabetic-upper": case "alphabetic-upper":
charset = utils.CharSetAlphabeticUpper charset = random.CharSetAlphabeticUpper
case "numeric-hex": case "numeric-hex":
charset = utils.CharSetNumericHex charset = random.CharSetNumericHex
case "numeric": case "numeric":
charset = utils.CharSetNumeric charset = random.CharSetNumeric
case "rfc3986": case "rfc3986":
charset = utils.CharSetRFC3986Unreserved charset = random.CharSetRFC3986Unreserved
case "rfc3986-lower": case "rfc3986-lower":
charset = utils.CharSetAlphabeticLower + utils.CharSetNumeric + utils.CharSetSymbolicRFC3986Unreserved charset = random.CharSetAlphabeticLower + random.CharSetNumeric + random.CharSetSymbolicRFC3986Unreserved
case "rfc3986-upper": case "rfc3986-upper":
charset = utils.CharSetAlphabeticUpper + utils.CharSetNumeric + utils.CharSetSymbolicRFC3986Unreserved charset = random.CharSetAlphabeticUpper + random.CharSetNumeric + random.CharSetSymbolicRFC3986Unreserved
default: default:
return "", fmt.Errorf("flag '--%s' with value '%s' is invalid, must be one of 'ascii', 'alphanumeric', 'alphabetic', 'numeric', 'numeric-hex', or 'rfc3986'", flagNameCharSet, c) return "", fmt.Errorf("flag '--%s' with value '%s' is invalid, must be one of 'ascii', 'alphanumeric', 'alphabetic', 'numeric', 'numeric-hex', or 'rfc3986'", flagNameCharSet, c)
} }
@ -109,7 +109,9 @@ func flagsGetRandomCharacters(flags *pflag.FlagSet, flagNameLength, flagNameChar
} }
} }
return utils.RandomString(n, charset), nil rand := &random.Cryptographical{}
return rand.StringCustom(n, charset), nil
} }
func termReadConfirmation(flags *pflag.FlagSet, name, prompt, confirmation string) (confirmed bool, err error) { func termReadConfirmation(flags *pflag.FlagSet, name, prompt, confirmation string) (confirmed bool, err error) {

View File

@ -1394,15 +1394,9 @@ notifier:
## Sets the client to public. This should typically not be set, please see the documentation for usage. ## Sets the client to public. This should typically not be set, please see the documentation for usage.
# public: false # public: false
## The policy to require for this client; one_factor or two_factor. ## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# authorization_policy: two_factor # redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
## Audience this client is allowed to request. ## Audience this client is allowed to request.
# audience: [] # audience: []
@ -1414,10 +1408,6 @@ notifier:
# - email # - email
# - profile # - profile
## Redirect URI's specifies a list of valid case-sensitive callbacks for this client.
# redirect_uris:
# - https://oidc.example.com:8080/oauth2/callback
## Grant Types configures which grants this client can obtain. ## Grant Types configures which grants this client can obtain.
## It's not recommended to define this unless you know what you're doing. ## It's not recommended to define this unless you know what you're doing.
# grant_types: # grant_types:
@ -1435,6 +1425,23 @@ notifier:
# - query # - query
# - fragment # - fragment
## The policy to require for this client; one_factor or two_factor.
# authorization_policy: two_factor
## Enforces the use of PKCE for this client when set to true.
# enforce_pkce: false
## Enforces the use of PKCE for this client when configured, and enforces the specified challenge method.
## Options are 'plain' and 'S256'.
# pkce_challenge_method: S256
## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256. ## The algorithm used to sign userinfo endpoint responses for this client, either none or RS256.
# userinfo_signing_algorithm: none # userinfo_signing_algorithm: none
## The consent mode controls how consent is obtained.
# consent_mode: auto
## This value controls the duration a consent on this client remains remembered when the consent mode is
## configured as 'auto' or 'pre-configured'.
# pre_configured_consent_duration: 1w
... ...

View File

@ -57,10 +57,13 @@ type OpenIDConnectClientConfiguration struct {
ResponseTypes []string `koanf:"response_types"` ResponseTypes []string `koanf:"response_types"`
ResponseModes []string `koanf:"response_modes"` ResponseModes []string `koanf:"response_modes"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
Policy string `koanf:"authorization_policy"` Policy string `koanf:"authorization_policy"`
EnforcePKCE bool `koanf:"enforce_pkce"`
PKCEChallengeMethod string `koanf:"pkce_challenge_method"`
UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"`
ConsentMode string `koanf:"consent_mode"` ConsentMode string `koanf:"consent_mode"`
ConsentPreConfiguredDuration *time.Duration `koanf:"pre_configured_consent_duration"` ConsentPreConfiguredDuration *time.Duration `koanf:"pre_configured_consent_duration"`
} }

View File

@ -43,8 +43,10 @@ var Keys = []string{
"identity_providers.oidc.clients[].grant_types", "identity_providers.oidc.clients[].grant_types",
"identity_providers.oidc.clients[].response_types", "identity_providers.oidc.clients[].response_types",
"identity_providers.oidc.clients[].response_modes", "identity_providers.oidc.clients[].response_modes",
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
"identity_providers.oidc.clients[].authorization_policy", "identity_providers.oidc.clients[].authorization_policy",
"identity_providers.oidc.clients[].enforce_pkce",
"identity_providers.oidc.clients[].pkce_challenge_method",
"identity_providers.oidc.clients[].userinfo_signing_algorithm",
"identity_providers.oidc.clients[].consent_mode", "identity_providers.oidc.clients[].consent_mode",
"identity_providers.oidc.clients[].pre_configured_consent_duration", "identity_providers.oidc.clients[].pre_configured_consent_duration",
"authentication_backend.password_reset.disable", "authentication_backend.password_reset.disable",

View File

@ -6,7 +6,6 @@ import (
"github.com/go-webauthn/webauthn/protocol" "github.com/go-webauthn/webauthn/protocol"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
) )
@ -172,6 +171,8 @@ const (
"invalid value: redirect uri '%s' must have the scheme but it is absent" "invalid value: redirect uri '%s' must have the scheme but it is absent"
errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " + errFmtOIDCClientInvalidPolicy = "identity_providers: oidc: client '%s': option 'policy' must be 'one_factor' " +
"or 'two_factor' but it is configured as '%s'" "or 'two_factor' but it is configured as '%s'"
errFmtOIDCClientInvalidPKCEChallengeMethod = "identity_providers: oidc: client '%s': option 'pkce_challenge_method' must be 'plain' " +
"or 'S256' but it is configured as '%s'"
errFmtOIDCClientInvalidConsentMode = "identity_providers: oidc: client '%s': consent: option 'mode' must be one of " + errFmtOIDCClientInvalidConsentMode = "identity_providers: oidc: client '%s': consent: option 'mode' must be one of " +
"'%s' but it is configured as '%s'" "'%s' but it is configured as '%s'"
errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " + errFmtOIDCClientInvalidEntry = "identity_providers: oidc: client '%s': option '%s' must only have the values " +

View File

@ -175,6 +175,13 @@ func validateOIDCClients(config *schema.OpenIDConnectConfiguration, val *schema.
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy)) val.Push(fmt.Errorf(errFmtOIDCClientInvalidPolicy, client.ID, client.Policy))
} }
switch client.PKCEChallengeMethod {
case "", "plain", "S256":
break
default:
val.Push(fmt.Errorf(errFmtOIDCClientInvalidPKCEChallengeMethod, client.ID, client.PKCEChallengeMethod))
}
validateOIDCClientConsentMode(c, config, val) validateOIDCClientConsentMode(c, config, val)
validateOIDCClientSectorIdentifier(client, val) validateOIDCClientSectorIdentifier(client, val)
validateOIDCClientScopes(c, config, val) validateOIDCClientScopes(c, config, val)

View File

@ -331,6 +331,40 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) {
fmt.Sprintf(errFmtOIDCClientInvalidConsentMode, "client-bad-consent-mode", strings.Join(append(validOIDCClientConsentModes, "auto"), "', '"), "cap"), fmt.Sprintf(errFmtOIDCClientInvalidConsentMode, "client-bad-consent-mode", strings.Join(append(validOIDCClientConsentModes, "auto"), "', '"), "cap"),
}, },
}, },
{
Name: "InvalidPKCEChallengeMethod",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode",
Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "abc",
},
},
Errors: []string{
fmt.Sprintf(errFmtOIDCClientInvalidPKCEChallengeMethod, "client-bad-pkce-mode", "abc"),
},
},
{
Name: "InvalidPKCEChallengeMethodLowerCaseS256",
Clients: []schema.OpenIDConnectClientConfiguration{
{
ID: "client-bad-pkce-mode-s256",
Secret: MustDecodeSecret("$plaintext$a-secret"),
Policy: policyTwoFactor,
RedirectURIs: []string{
"https://google.com",
},
PKCEChallengeMethod: "s256",
},
},
Errors: []string{
fmt.Sprintf(errFmtOIDCClientInvalidPKCEChallengeMethod, "client-bad-pkce-mode-s256", "s256"),
},
},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -609,7 +643,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi
assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURIPublic, "client-with-bad-redirect-uri", oauth2InstalledApp)) assert.EqualError(t, validator.Errors()[1], fmt.Sprintf(errFmtOIDCClientRedirectURIPublic, "client-with-bad-redirect-uri", oauth2InstalledApp))
} }
func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *testing.T) { func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *testing.T) {
validator := schema.NewStructValidator() validator := schema.NewStructValidator()
config := &schema.IdentityProvidersConfiguration{ config := &schema.IdentityProvidersConfiguration{
OIDC: &schema.OpenIDConnectConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{
@ -640,6 +674,24 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidPublicClients(t *te
"http://127.0.0.1", "http://127.0.0.1",
}, },
}, },
{
ID: "client-with-pkce-mode-plain",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "plain",
},
{
ID: "client-with-pkce-mode-S256",
Public: true,
Policy: "two_factor",
RedirectURIs: []string{
"https://pkce.com",
},
PKCEChallengeMethod: "S256",
},
}, },
}, },
} }

View File

@ -63,3 +63,33 @@ func TestCheckSafeRedirection_SafeRedirection(t *testing.T) {
OK: true, OK: true,
}) })
} }
func TestShouldFailOnInvalidBody(t *testing.T) {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})
defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
mock.SetRequestBody(t, "not a valid json")
CheckSafeRedirectionPOST(mock.Ctx)
mock.Assert200KO(t, "Operation failed.")
}
func TestShouldFailOnInvalidURL(t *testing.T) {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})
defer mock.Close()
mock.Ctx.Configuration.Session.Domain = exampleDotComDomain
mock.SetRequestBody(t, checkURIWithinDomainRequestBody{
URI: "https//invalid-url",
})
CheckSafeRedirectionPOST(mock.Ctx)
mock.Assert200KO(t, "Operation failed.")
}

View File

@ -0,0 +1,83 @@
package handlers
import (
// "strings".
"encoding/json"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/authelia/authelia/v4/internal/mocks"
)
type passwordPolicyResponseBody struct {
Status string
Data PasswordPolicyBody
}
type PasswordPolicySuite struct {
suite.Suite
mock *mocks.MockAutheliaCtx
}
func (s *PasswordPolicySuite) SetupTest() {
s.mock = mocks.NewMockAutheliaCtx(s.T())
}
func (s *PasswordPolicySuite) TearDownTest() {
s.mock.Close()
}
func (s *PasswordPolicySuite) TestShouldBeDisabled() {
s.mock.Ctx.Configuration.PasswordPolicy.ZXCVBN.Enabled = false
s.mock.Ctx.Configuration.PasswordPolicy.Standard.Enabled = false
PasswordPolicyConfigurationGET(s.mock.Ctx)
response := &passwordPolicyResponseBody{}
err := json.Unmarshal(s.mock.Ctx.Response.Body(), response)
require.NoError(s.T(), err)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "disabled", response.Data.Mode)
}
func (s *PasswordPolicySuite) TestShouldBeStandard() {
s.mock.Ctx.Configuration.PasswordPolicy.ZXCVBN.Enabled = false
s.mock.Ctx.Configuration.PasswordPolicy.Standard.Enabled = true
s.mock.Ctx.Configuration.PasswordPolicy.Standard.MinLength = 4
s.mock.Ctx.Configuration.PasswordPolicy.Standard.MaxLength = 8
PasswordPolicyConfigurationGET(s.mock.Ctx)
response := &passwordPolicyResponseBody{}
err := json.Unmarshal(s.mock.Ctx.Response.Body(), response)
require.NoError(s.T(), err)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "standard", response.Data.Mode)
assert.Equal(s.T(), 4, response.Data.MinLength)
assert.Equal(s.T(), 8, response.Data.MaxLength)
}
func (s *PasswordPolicySuite) TestShouldBeZXCVBN() {
s.mock.Ctx.Configuration.PasswordPolicy.ZXCVBN.Enabled = true
s.mock.Ctx.Configuration.PasswordPolicy.Standard.Enabled = false
PasswordPolicyConfigurationGET(s.mock.Ctx)
response := &passwordPolicyResponseBody{}
err := json.Unmarshal(s.mock.Ctx.Response.Body(), response)
require.NoError(s.T(), err)
assert.Equal(s.T(), 200, s.mock.Ctx.Response.StatusCode())
assert.Equal(s.T(), "zxcvbn", response.Data.Mode)
}
func TestRunPasswordPolicySuite(t *testing.T) {
s := new(PasswordPolicySuite)
suite.Run(t, s)
}

View File

@ -0,0 +1,26 @@
package handlers
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/session"
)
var okMessageBytes = []byte("{\"status\":\"OK\"}")
func TestHealthOk(t *testing.T) {
mock := mocks.NewMockAutheliaCtxWithUserSession(t, session.UserSession{
Username: "john",
AuthenticationLevel: authentication.OneFactor,
})
defer mock.Close()
HealthGET(mock.Ctx)
assert.Equal(t, 200, mock.Ctx.Response.StatusCode())
assert.Equal(t, okMessageBytes, mock.Ctx.Response.Body())
}

View File

@ -52,6 +52,16 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr
return return
} }
if err = client.ValidateAuthorizationPolicy(requester); err != nil {
rfc := fosite.ErrorToRFC6749Error(err)
ctx.Logger.Errorf("Authorization Request with id '%s' on client with id '%s' failed to validate the authorization policy: %s", requester.GetID(), clientID, rfc.WithExposeDebug(true).GetDescription())
ctx.Providers.OpenIDConnect.WriteAuthorizeError(ctx, rw, requester, err)
return
}
issuer = ctx.RootURL() issuer = ctx.RootURL()
userSession := ctx.GetSession() userSession := ctx.GetSession()

View File

@ -407,3 +407,8 @@ func (ctx *AutheliaCtx) SetContentTypeTextHTML() {
func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() { func (ctx *AutheliaCtx) SetContentTypeApplicationJSON() {
ctx.SetContentTypeBytes(contentTypeApplicationJSON) ctx.SetContentTypeBytes(contentTypeApplicationJSON)
} }
// SetContentTypeApplicationYAML efficiently sets the Content-Type header to 'application/yaml; charset=utf-8'.
func (ctx *AutheliaCtx) SetContentTypeApplicationYAML() {
ctx.SetContentTypeBytes(contentTypeApplicationYAML)
}

View File

@ -13,9 +13,66 @@ import (
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
"github.com/authelia/authelia/v4/internal/model" "github.com/authelia/authelia/v4/internal/model"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
) )
func TestContentTypes(t *testing.T) {
testCases := []struct {
name string
setup func(ctx *middlewares.AutheliaCtx) (err error)
expected string
}{
{
name: "ApplicationJSON",
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
ctx.SetContentTypeApplicationJSON()
return nil
},
expected: "application/json; charset=utf-8",
},
{
name: "ApplicationYAML",
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
ctx.SetContentTypeApplicationYAML()
return nil
},
expected: "application/yaml; charset=utf-8",
},
{
name: "TextPlain",
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
ctx.SetContentTypeTextPlain()
return nil
},
expected: "text/plain; charset=utf-8",
},
{
name: "TextHTML",
setup: func(ctx *middlewares.AutheliaCtx) (err error) {
ctx.SetContentTypeTextHTML()
return nil
},
expected: "text/html; charset=utf-8",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close()
assert.NoError(t, tc.setup(mock.Ctx))
assert.Equal(t, tc.expected, string(mock.Ctx.Response.Header.ContentType()))
})
}
}
func TestIssuerURL(t *testing.T) { func TestIssuerURL(t *testing.T) {
testCases := []struct { testCases := []struct {
name string name string
@ -44,8 +101,8 @@ func TestIssuerURL(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", tc.proto) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, tc.proto)
mock.Ctx.Request.Header.Set("X-Forwarded-Host", tc.host) mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, tc.host)
if tc.base != "" { if tc.base != "" {
mock.Ctx.SetUserValue("base_url", tc.base) mock.Ctx.SetUserValue("base_url", tc.base)
@ -72,6 +129,7 @@ func TestShouldCallNextWithAutheliaCtx(t *testing.T) {
providers := middlewares.Providers{ providers := middlewares.Providers{
UserProvider: userProvider, UserProvider: userProvider,
SessionProvider: sessionProvider, SessionProvider: sessionProvider,
Random: random.NewMathematical(),
} }
nextCalled := false nextCalled := false
@ -103,8 +161,8 @@ func TestShouldGetOriginalURLFromOriginalURLHeader(t *testing.T) {
func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) { func TestShouldGetOriginalURLFromForwardedHeadersWithoutURI(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
defer mock.Close() defer mock.Close()
mock.Ctx.Request.Header.Set("X-Forwarded-Proto", "https") mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
mock.Ctx.Request.Header.Set("X-Forwarded-Host", "home.example.com") mock.Ctx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "home.example.com")
originalURL, err := mock.Ctx.GetOriginalURL() originalURL, err := mock.Ctx.GetOriginalURL()
assert.NoError(t, err) assert.NoError(t, err)
@ -142,7 +200,7 @@ func TestShouldOnlyFallbackToNonXForwardedHeadersWhenNil(t *testing.T) {
mock.Ctx.RequestCtx.Request.SetHost("localhost") mock.Ctx.RequestCtx.Request.SetHost("localhost")
mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234") mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedHost, "auth.example.com:1234")
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-URI", "/base/2fa/one-time-password") mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-URI", "/base/2fa/one-time-password")
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Proto", "https") mock.Ctx.RequestCtx.Request.Header.Set(fasthttp.HeaderXForwardedProto, "https")
mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Method", "GET") mock.Ctx.RequestCtx.Request.Header.Set("X-Forwarded-Method", "GET")
assert.Equal(t, []byte("https"), mock.Ctx.XForwardedProto()) assert.Equal(t, []byte("https"), mock.Ctx.XForwardedProto())

View File

@ -88,6 +88,7 @@ var (
contentTypeTextPlain = []byte("text/plain; charset=utf-8") contentTypeTextPlain = []byte("text/plain; charset=utf-8")
contentTypeTextHTML = []byte("text/html; charset=utf-8") contentTypeTextHTML = []byte("text/html; charset=utf-8")
contentTypeApplicationJSON = []byte("application/json; charset=utf-8") contentTypeApplicationJSON = []byte("application/json; charset=utf-8")
contentTypeApplicationYAML = []byte("application/yaml; charset=utf-8")
) )
const ( const (

View File

@ -11,6 +11,7 @@ import (
"github.com/google/uuid" "github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/mocks" "github.com/authelia/authelia/v4/internal/mocks"
@ -73,8 +74,8 @@ func TestShouldFailSendingAnEmail(t *testing.T) {
defer mock.Close() defer mock.Close()
mock.Ctx.Configuration.JWTSecret = testJWTSecret mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") mock.Ctx.Request.Header.Add(fasthttp.HeaderXForwardedProto, "http")
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host") mock.Ctx.Request.Header.Add(fasthttp.HeaderXForwardedHost, "host")
mock.StorageMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).
@ -95,8 +96,8 @@ func TestShouldSucceedIdentityVerificationStartProcess(t *testing.T) {
mock := mocks.NewMockAutheliaCtx(t) mock := mocks.NewMockAutheliaCtx(t)
mock.Ctx.Configuration.JWTSecret = testJWTSecret mock.Ctx.Configuration.JWTSecret = testJWTSecret
mock.Ctx.Request.Header.Add("X-Forwarded-Proto", "http") mock.Ctx.Request.Header.Add(fasthttp.HeaderXForwardedProto, "http")
mock.Ctx.Request.Header.Add("X-Forwarded-Host", "host") mock.Ctx.Request.Header.Add(fasthttp.HeaderXForwardedHost, "host")
mock.StorageMock.EXPECT(). mock.StorageMock.EXPECT().
SaveIdentityVerification(mock.Ctx, gomock.Any()). SaveIdentityVerification(mock.Ctx, gomock.Any()).

View File

@ -1,7 +1,6 @@
package middlewares package middlewares
import ( import (
"crypto/rand"
"math" "math"
"math/big" "math/big"
"sync" "sync"
@ -62,7 +61,7 @@ func movingAverageIteration(value time.Duration, history int, successful bool, c
} }
func calculateActualDelay(ctx *AutheliaCtx, execDuration time.Duration, execDurationAvgMs, minDelayMs float64, maxRandomMs int64, successful bool) (actualDelayMs float64) { func calculateActualDelay(ctx *AutheliaCtx, execDuration time.Duration, execDurationAvgMs, minDelayMs float64, maxRandomMs int64, successful bool) (actualDelayMs float64) {
randomDelayMs, err := rand.Int(rand.Reader, big.NewInt(maxRandomMs)) randomDelayMs, err := ctx.Providers.Random.IntErr(big.NewInt(maxRandomMs))
if err != nil { if err != nil {
return float64(maxRandomMs) return float64(maxRandomMs)
} }

View File

@ -9,6 +9,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/random"
) )
func TestTimingAttackDelayAverages(t *testing.T) { func TestTimingAttackDelayAverages(t *testing.T) {
@ -45,7 +46,12 @@ func TestTimingAttackDelayCalculations(t *testing.T) {
avgExecDurationMs := 1000.0 avgExecDurationMs := 1000.0
expectedMinimumDelayMs := avgExecDurationMs - float64(execDuration.Milliseconds()) expectedMinimumDelayMs := avgExecDurationMs - float64(execDuration.Milliseconds())
ctx := &AutheliaCtx{Logger: logging.Logger().WithFields(logrus.Fields{})} ctx := &AutheliaCtx{
Logger: logging.Logger().WithFields(logrus.Fields{}),
Providers: Providers{
Random: &random.Cryptographical{},
},
}
for i := 0; i < 100; i++ { for i := 0; i < 100; i++ {
delay := calculateActualDelay(ctx, execDuration, avgExecDurationMs, 250, 85, false) delay := calculateActualDelay(ctx, execDuration, avgExecDurationMs, 250, 85, false)

View File

@ -11,6 +11,7 @@ import (
"github.com/authelia/authelia/v4/internal/notification" "github.com/authelia/authelia/v4/internal/notification"
"github.com/authelia/authelia/v4/internal/ntp" "github.com/authelia/authelia/v4/internal/ntp"
"github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/oidc"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/storage" "github.com/authelia/authelia/v4/internal/storage"
@ -44,6 +45,7 @@ type Providers struct {
Templates *templates.Provider Templates *templates.Provider
TOTP totp.Provider TOTP totp.Provider
PasswordPolicy PasswordPolicyProvider PasswordPolicy PasswordPolicyProvider
Random random.Provider
} }
// RequestHandler represents an Authelia request handler. // RequestHandler represents an Authelia request handler.

View File

@ -16,6 +16,7 @@ import (
"github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/authorization"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/regulation" "github.com/authelia/authelia/v4/internal/regulation"
"github.com/authelia/authelia/v4/internal/session" "github.com/authelia/authelia/v4/internal/session"
"github.com/authelia/authelia/v4/internal/templates" "github.com/authelia/authelia/v4/internal/templates"
@ -34,6 +35,7 @@ type MockAutheliaCtx struct {
StorageMock *MockStorage StorageMock *MockStorage
NotifierMock *MockNotifier NotifierMock *MockNotifier
TOTPMock *MockTOTP TOTPMock *MockTOTP
RandomMock *MockRandom
UserSession *session.UserSession UserSession *session.UserSession
@ -98,6 +100,10 @@ func NewMockAutheliaCtx(t *testing.T) *MockAutheliaCtx {
mockAuthelia.TOTPMock = NewMockTOTP(mockAuthelia.Ctrl) mockAuthelia.TOTPMock = NewMockTOTP(mockAuthelia.Ctrl)
providers.TOTP = mockAuthelia.TOTPMock providers.TOTP = mockAuthelia.TOTPMock
mockAuthelia.RandomMock = NewMockRandom(mockAuthelia.Ctrl)
providers.Random = random.NewMathematical()
var err error var err error
if providers.Templates, err = templates.New(templates.Config{}); err != nil { if providers.Templates, err = templates.New(templates.Config{}); err != nil {

View File

@ -8,3 +8,4 @@ package mocks
//go:generate mockgen -package mocks -destination totp.go -mock_names Provider=MockTOTP github.com/authelia/authelia/v4/internal/totp Provider //go:generate mockgen -package mocks -destination totp.go -mock_names Provider=MockTOTP github.com/authelia/authelia/v4/internal/totp Provider
//go:generate mockgen -package mocks -destination storage.go -mock_names Provider=MockStorage github.com/authelia/authelia/v4/internal/storage Provider //go:generate mockgen -package mocks -destination storage.go -mock_names Provider=MockStorage github.com/authelia/authelia/v4/internal/storage Provider
//go:generate mockgen -package mocks -destination duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API //go:generate mockgen -package mocks -destination duo_api.go -mock_names API=MockAPI github.com/authelia/authelia/v4/internal/duo API
//go:generate mockgen -package mocks -destination random.go -mock_names Provider=MockRandom github.com/authelia/authelia/v4/internal/random Provider

View File

@ -0,0 +1,195 @@
// Code generated by MockGen. DO NOT EDIT.
// Source: github.com/authelia/authelia/v4/internal/random (interfaces: Provider)
// Package mocks is a generated GoMock package.
package mocks
import (
big "math/big"
reflect "reflect"
gomock "github.com/golang/mock/gomock"
)
// MockRandom is a mock of Provider interface.
type MockRandom struct {
ctrl *gomock.Controller
recorder *MockRandomMockRecorder
}
// MockRandomMockRecorder is the mock recorder for MockRandom.
type MockRandomMockRecorder struct {
mock *MockRandom
}
// NewMockRandom creates a new mock instance.
func NewMockRandom(ctrl *gomock.Controller) *MockRandom {
mock := &MockRandom{ctrl: ctrl}
mock.recorder = &MockRandomMockRecorder{mock}
return mock
}
// EXPECT returns an object that allows the caller to indicate expected use.
func (m *MockRandom) EXPECT() *MockRandomMockRecorder {
return m.recorder
}
// Bytes mocks base method.
func (m *MockRandom) Bytes() []byte {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Bytes")
ret0, _ := ret[0].([]byte)
return ret0
}
// Bytes indicates an expected call of Bytes.
func (mr *MockRandomMockRecorder) Bytes() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Bytes", reflect.TypeOf((*MockRandom)(nil).Bytes))
}
// BytesCustom mocks base method.
func (m *MockRandom) BytesCustom(arg0 int, arg1 []byte) []byte {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BytesCustom", arg0, arg1)
ret0, _ := ret[0].([]byte)
return ret0
}
// BytesCustom indicates an expected call of BytesCustom.
func (mr *MockRandomMockRecorder) BytesCustom(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BytesCustom", reflect.TypeOf((*MockRandom)(nil).BytesCustom), arg0, arg1)
}
// BytesCustomErr mocks base method.
func (m *MockRandom) BytesCustomErr(arg0 int, arg1 []byte) ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BytesCustomErr", arg0, arg1)
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BytesCustomErr indicates an expected call of BytesCustomErr.
func (mr *MockRandomMockRecorder) BytesCustomErr(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BytesCustomErr", reflect.TypeOf((*MockRandom)(nil).BytesCustomErr), arg0, arg1)
}
// BytesErr mocks base method.
func (m *MockRandom) BytesErr() ([]byte, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "BytesErr")
ret0, _ := ret[0].([]byte)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// BytesErr indicates an expected call of BytesErr.
func (mr *MockRandomMockRecorder) BytesErr() *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BytesErr", reflect.TypeOf((*MockRandom)(nil).BytesErr))
}
// Int mocks base method.
func (m *MockRandom) Int(arg0 *big.Int) *big.Int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Int", arg0)
ret0, _ := ret[0].(*big.Int)
return ret0
}
// Int indicates an expected call of Int.
func (mr *MockRandomMockRecorder) Int(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Int", reflect.TypeOf((*MockRandom)(nil).Int), arg0)
}
// IntErr mocks base method.
func (m *MockRandom) IntErr(arg0 *big.Int) (*big.Int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IntErr", arg0)
ret0, _ := ret[0].(*big.Int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IntErr indicates an expected call of IntErr.
func (mr *MockRandomMockRecorder) IntErr(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntErr", reflect.TypeOf((*MockRandom)(nil).IntErr), arg0)
}
// Integer mocks base method.
func (m *MockRandom) Integer(arg0 int) int {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Integer", arg0)
ret0, _ := ret[0].(int)
return ret0
}
// Integer indicates an expected call of Integer.
func (mr *MockRandomMockRecorder) Integer(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Integer", reflect.TypeOf((*MockRandom)(nil).Integer), arg0)
}
// IntegerErr mocks base method.
func (m *MockRandom) IntegerErr(arg0 int) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "IntegerErr", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// IntegerErr indicates an expected call of IntegerErr.
func (mr *MockRandomMockRecorder) IntegerErr(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "IntegerErr", reflect.TypeOf((*MockRandom)(nil).IntegerErr), arg0)
}
// Read mocks base method.
func (m *MockRandom) Read(arg0 []byte) (int, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Read", arg0)
ret0, _ := ret[0].(int)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// Read indicates an expected call of Read.
func (mr *MockRandomMockRecorder) Read(arg0 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Read", reflect.TypeOf((*MockRandom)(nil).Read), arg0)
}
// StringCustom mocks base method.
func (m *MockRandom) StringCustom(arg0 int, arg1 string) string {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StringCustom", arg0, arg1)
ret0, _ := ret[0].(string)
return ret0
}
// StringCustom indicates an expected call of StringCustom.
func (mr *MockRandomMockRecorder) StringCustom(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringCustom", reflect.TypeOf((*MockRandom)(nil).StringCustom), arg0, arg1)
}
// StringCustomErr mocks base method.
func (m *MockRandom) StringCustomErr(arg0 int, arg1 string) (string, error) {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "StringCustomErr", arg0, arg1)
ret0, _ := ret[0].(string)
ret1, _ := ret[1].(error)
return ret0, ret1
}
// StringCustomErr indicates an expected call of StringCustomErr.
func (mr *MockRandomMockRecorder) StringCustomErr(arg0, arg1 interface{}) *gomock.Call {
mr.mock.ctrl.T.Helper()
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StringCustomErr", reflect.TypeOf((*MockRandom)(nil).StringCustomErr), arg0, arg1)
}

View File

@ -14,6 +14,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/templates" "github.com/authelia/authelia/v4/internal/templates"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -64,6 +65,7 @@ func NewSMTPNotifier(config *schema.SMTPNotifierConfiguration, certPool *x509.Ce
return &SMTPNotifier{ return &SMTPNotifier{
config: config, config: config,
domain: domain, domain: domain,
random: &random.Cryptographical{},
tls: utils.NewTLSConfig(config.TLS, certPool), tls: utils.NewTLSConfig(config.TLS, certPool),
log: logging.Logger(), log: logging.Logger(),
opts: opts, opts: opts,
@ -74,6 +76,7 @@ func NewSMTPNotifier(config *schema.SMTPNotifierConfiguration, certPool *x509.Ce
type SMTPNotifier struct { type SMTPNotifier struct {
config *schema.SMTPNotifierConfiguration config *schema.SMTPNotifierConfiguration
domain string domain string
random random.Provider
tls *tls.Config tls *tls.Config
log *logrus.Logger log *logrus.Logger
opts []gomail.Option opts []gomail.Option
@ -104,10 +107,10 @@ func (n *SMTPNotifier) StartupCheck() (err error) {
func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject string, et *templates.EmailTemplate, data any) (err error) { func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject string, et *templates.EmailTemplate, data any) (err error) {
msg := gomail.NewMsg( msg := gomail.NewMsg(
gomail.WithMIMEVersion(gomail.Mime10), gomail.WithMIMEVersion(gomail.Mime10),
gomail.WithBoundary(utils.RandomString(30, utils.CharSetAlphaNumeric)), gomail.WithBoundary(n.random.StringCustom(30, random.CharSetAlphaNumeric)),
) )
setMessageID(msg, n.domain) n.setMessageID(msg, n.domain)
if err = msg.From(n.config.Sender.String()); err != nil { if err = msg.From(n.config.Sender.String()); err != nil {
return fmt.Errorf("notifier: smtp: failed to set from address: %w", err) return fmt.Errorf("notifier: smtp: failed to set from address: %w", err)
@ -161,10 +164,10 @@ func (n *SMTPNotifier) Send(ctx context.Context, recipient mail.Address, subject
return nil return nil
} }
func setMessageID(msg *gomail.Msg, domain string) { func (n *SMTPNotifier) setMessageID(msg *gomail.Msg, domain string) {
rn, _ := utils.RandomInt(100000000) rn := n.random.Integer(100000000)
rm, _ := utils.RandomInt(10000) rm := n.random.Integer(10000)
rs := utils.RandomString(17, utils.CharSetAlphaNumeric) rs := n.random.StringCustom(17, random.CharSetAlphaNumeric)
pid := os.Getpid() + rm pid := os.Getpid() + rm
msg.SetMessageIDWithValue(fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, domain)) msg.SetMessageIDWithValue(fmt.Sprintf("%d.%d%d.%s@%s", pid, rn, rm, rs, domain))

View File

@ -10,7 +10,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/validator" "github.com/authelia/authelia/v4/internal/configuration/validator"
) )
func TestShouldCheckNTP(t *testing.T) { func TestShouldCheckNTPV4(t *testing.T) {
config := &schema.Configuration{ config := &schema.Configuration{
NTP: schema.NTPConfiguration{ NTP: schema.NTPConfiguration{
Address: "time.cloudflare.com:123", Address: "time.cloudflare.com:123",
@ -26,3 +26,20 @@ func TestShouldCheckNTP(t *testing.T) {
assert.NoError(t, ntp.StartupCheck()) assert.NoError(t, ntp.StartupCheck())
} }
func TestShouldCheckNTPV3(t *testing.T) {
config := &schema.Configuration{
NTP: schema.NTPConfiguration{
Address: "time.cloudflare.com:123",
Version: 3,
MaximumDesync: time.Second * 3,
},
}
sv := schema.NewStructValidator()
validator.ValidateNTP(config, sv)
ntp := NewProvider(&config.NTP)
assert.NoError(t, ntp.StartupCheck())
}

View File

@ -9,8 +9,33 @@ import (
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
func TestShould(t *testing.T) { func TestNtpIsOffsetTooLarge(t *testing.T) {
maxOffset, _ := utils.ParseDurationString("1s") maxOffset, _ := utils.ParseDurationString("1s")
assert.True(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now().Add(time.Second*2))) assert.True(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now().Add(time.Second*2)))
assert.True(t, ntpIsOffsetTooLarge(maxOffset, time.Now().Add(time.Second*2), time.Now()))
assert.False(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now())) assert.False(t, ntpIsOffsetTooLarge(maxOffset, time.Now(), time.Now()))
} }
func TestNtpPacketToTime(t *testing.T) {
resp := &ntpPacket{
TxTimeSeconds: 60,
TxTimeFraction: 0,
}
expected := time.Unix(int64(float64(60)-ntpEpochOffset), 0)
ntpTime := ntpPacketToTime(resp)
assert.Equal(t, expected, ntpTime)
}
func TestLeapVersionClientMode(t *testing.T) {
v3Noleap := uint8(27)
v4Noleap := uint8(43)
v3leap := uint8(91)
v4leap := uint8(107)
assert.Equal(t, v3Noleap, ntpLeapVersionClientMode(false, ntpV3))
assert.Equal(t, v4Noleap, ntpLeapVersionClientMode(false, ntpV4))
assert.Equal(t, v3leap, ntpLeapVersionClientMode(true, ntpV3))
assert.Equal(t, v4leap, ntpLeapVersionClientMode(true, ntpV4))
}

View File

@ -1,7 +1,10 @@
package oidc package oidc
import ( import (
"fmt"
"github.com/ory/fosite" "github.com/ory/fosite"
"github.com/ory/x/errorsx"
"github.com/authelia/authelia/v4/internal/authentication" "github.com/authelia/authelia/v4/internal/authentication"
"github.com/authelia/authelia/v4/internal/authorization" "github.com/authelia/authelia/v4/internal/authorization"
@ -18,6 +21,10 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
SectorIdentifier: config.SectorIdentifier.String(), SectorIdentifier: config.SectorIdentifier.String(),
Public: config.Public, Public: config.Public,
EnforcePKCE: config.EnforcePKCE || config.PKCEChallengeMethod != "",
EnforcePKCEChallengeMethod: config.PKCEChallengeMethod != "",
PKCEChallengeMethod: config.PKCEChallengeMethod,
Audience: config.Audience, Audience: config.Audience,
Scopes: config.Scopes, Scopes: config.Scopes,
RedirectURIs: config.RedirectURIs, RedirectURIs: config.RedirectURIs,
@ -39,6 +46,29 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client *Client)
return client return client
} }
// ValidateAuthorizationPolicy is a helper function to validate additional policy constraints on a per-client basis.
func (c *Client) ValidateAuthorizationPolicy(r fosite.Requester) (err error) {
form := r.GetRequestForm()
if c.EnforcePKCE {
if form.Get("code_challenge") == "" {
return errorsx.WithStack(fosite.ErrInvalidRequest.
WithHint("Clients must include a code_challenge when performing the authorize code flow, but it is missing.").
WithDebug("The server is configured in a way that enforces PKCE for this client."))
}
if c.EnforcePKCEChallengeMethod {
if method := form.Get("code_challenge_method"); method != c.PKCEChallengeMethod {
return errorsx.WithStack(fosite.ErrInvalidRequest.
WithHint(fmt.Sprintf("Client must use code_challenge_method=%s, %s is not allowed.", c.PKCEChallengeMethod, method)).
WithDebug(fmt.Sprintf("The server is configured in a way that enforces PKCE %s as challenge method for this client.", c.PKCEChallengeMethod)))
}
}
}
return nil
}
// IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient. // IsAuthenticationLevelSufficient returns if the provided authentication.Level is sufficient for the client of the AutheliaClient.
func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool { func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) bool {
if level == authentication.NotAuthenticated { if level == authentication.NotAuthenticated {
@ -48,11 +78,6 @@ func (c *Client) IsAuthenticationLevelSufficient(level authentication.Level) boo
return authorization.IsAuthLevelSufficient(level, c.Policy) return authorization.IsAuthLevelSufficient(level, c.Policy)
} }
// GetID returns the ID.
func (c *Client) GetID() string {
return c.ID
}
// GetSectorIdentifier returns the SectorIdentifier for this client. // GetSectorIdentifier returns the SectorIdentifier for this client.
func (c *Client) GetSectorIdentifier() string { func (c *Client) GetSectorIdentifier() string {
return c.SectorIdentifier return c.SectorIdentifier
@ -74,6 +99,11 @@ func (c *Client) GetConsentResponseBody(consent *model.OAuth2ConsentSession) Con
return body return body
} }
// GetID returns the ID.
func (c *Client) GetID() string {
return c.ID
}
// GetHashedSecret returns the Secret. // GetHashedSecret returns the Secret.
func (c *Client) GetHashedSecret() []byte { func (c *Client) GetHashedSecret() []byte {
if c.Secret == nil { if c.Secret == nil {

View File

@ -217,6 +217,90 @@ func TestClient_GetResponseTypes(t *testing.T) {
assert.Equal(t, "id_token", responseTypes[1]) assert.Equal(t, "id_token", responseTypes[1])
} }
func TestNewClientPKCE(t *testing.T) {
testCases := []struct {
name string
have schema.OpenIDConnectClientConfiguration
expectedEnforcePKCE bool
expectedEnforcePKCEChallengeMethod bool
expected string
req *fosite.Request
err string
}{
{
"ShouldNotEnforcePKCEAndNotErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{},
false,
false,
"",
&fosite.Request{},
"",
},
{
"ShouldEnforcePKCEAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true},
true,
false,
"",
&fosite.Request{},
"invalid_request",
},
{
"ShouldEnforcePKCEAndNotErrorOnPKCERequest",
schema.OpenIDConnectClientConfiguration{EnforcePKCE: true},
true,
false,
"",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
"",
},
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnNonPKCERequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{},
"invalid_request",
},
{"ShouldEnforcePKCEFromChallengeMethodAndErrorOnInvalidChallengeMethod",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}}},
"invalid_request",
},
{"ShouldEnforcePKCEFromChallengeMethodAndNotErrorOnValidRequest",
schema.OpenIDConnectClientConfiguration{PKCEChallengeMethod: "S256"},
true,
true,
"S256",
&fosite.Request{Form: map[string][]string{"code_challenge": {"abc"}, "code_challenge_method": {"S256"}}},
"",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
client := NewClient(tc.have)
assert.Equal(t, tc.expectedEnforcePKCE, client.EnforcePKCE)
assert.Equal(t, tc.expectedEnforcePKCEChallengeMethod, client.EnforcePKCEChallengeMethod)
assert.Equal(t, tc.expected, client.PKCEChallengeMethod)
if tc.req != nil {
err := client.ValidateAuthorizationPolicy(tc.req)
if tc.err != "" {
assert.EqualError(t, err, tc.err)
} else {
assert.NoError(t, err)
}
}
})
}
}
func TestClient_IsPublic(t *testing.T) { func TestClient_IsPublic(t *testing.T) {
c := Client{} c := Client{}

View File

@ -40,11 +40,10 @@ func NewConfig(config *schema.OpenIDConnectConfiguration) *Config {
}, },
} }
prefix := "authelia_%s_"
c.Strategy.Core = &HMACCoreStrategy{ c.Strategy.Core = &HMACCoreStrategy{
Enigma: &hmac.HMACStrategy{Config: c}, Enigma: &hmac.HMACStrategy{Config: c},
Config: c, Config: c,
prefix: &prefix, prefix: tokenPrefixFmt,
} }
return c return c

View File

@ -106,6 +106,13 @@ const (
JWTHeaderKeyIdentifier = "kid" JWTHeaderKeyIdentifier = "kid"
) )
const (
tokenPrefixFmt = "authelia_%s_" //nolint:gosec
tokenPrefixPartAccessToken = "at"
tokenPrefixPartRefreshToken = "rt"
tokenPrefixPartAuthorizeCode = "ac"
)
// Paths. // Paths.
const ( const (
EndpointPathConsent = "/consent" EndpointPathConsent = "/consent"

View File

@ -19,7 +19,7 @@ type HMACCoreStrategy struct {
fosite.RefreshTokenLifespanProvider fosite.RefreshTokenLifespanProvider
fosite.AuthorizeCodeLifespanProvider fosite.AuthorizeCodeLifespanProvider
} }
prefix *string prefix string
} }
// AccessTokenSignature implements oauth2.AccessTokenStrategy. // AccessTokenSignature implements oauth2.AccessTokenStrategy.
@ -34,7 +34,7 @@ func (h *HMACCoreStrategy) GenerateAccessToken(ctx context.Context, _ fosite.Req
return "", "", err return "", "", err
} }
return h.setPrefix(token, "at"), sig, nil return h.setPrefix(token, tokenPrefixPartAccessToken), sig, nil
} }
// ValidateAccessToken implements oauth2.AccessTokenStrategy. // ValidateAccessToken implements oauth2.AccessTokenStrategy.
@ -48,7 +48,7 @@ func (h *HMACCoreStrategy) ValidateAccessToken(ctx context.Context, r fosite.Req
return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Access token expired at '%s'.", exp)) return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Access token expired at '%s'.", exp))
} }
return h.Enigma.Validate(ctx, h.trimPrefix(token, "at")) return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPrefixPartAccessToken))
} }
// RefreshTokenSignature implements oauth2.RefreshTokenStrategy. // RefreshTokenSignature implements oauth2.RefreshTokenStrategy.
@ -63,21 +63,22 @@ func (h *HMACCoreStrategy) GenerateRefreshToken(ctx context.Context, _ fosite.Re
return "", "", err return "", "", err
} }
return h.setPrefix(token, "rt"), sig, nil return h.setPrefix(token, tokenPrefixPartRefreshToken), sig, nil
} }
// ValidateRefreshToken implements oauth2.RefreshTokenStrategy. // ValidateRefreshToken implements oauth2.RefreshTokenStrategy.
func (h *HMACCoreStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) { func (h *HMACCoreStrategy) ValidateRefreshToken(ctx context.Context, r fosite.Requester, token string) (err error) {
var exp = r.GetSession().GetExpiresAt(fosite.RefreshToken) var exp = r.GetSession().GetExpiresAt(fosite.RefreshToken)
if exp.IsZero() { if exp.IsZero() {
return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt")) return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPrefixPartRefreshToken))
} }
if !exp.IsZero() && exp.Before(time.Now().UTC()) { if exp.Before(time.Now().UTC()) {
return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Refresh token expired at '%s'.", exp)) return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Refresh token expired at '%s'.", exp))
} }
return h.Enigma.Validate(ctx, h.trimPrefix(token, "rt")) return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPrefixPartRefreshToken))
} }
// AuthorizeCodeSignature implements oauth2.AuthorizeCodeStrategy. // AuthorizeCodeSignature implements oauth2.AuthorizeCodeStrategy.
@ -92,12 +93,13 @@ func (h *HMACCoreStrategy) GenerateAuthorizeCode(ctx context.Context, _ fosite.R
return "", "", err return "", "", err
} }
return h.setPrefix(token, "ac"), sig, nil return h.setPrefix(token, tokenPrefixPartAuthorizeCode), sig, nil
} }
// ValidateAuthorizeCode implements oauth2.AuthorizeCodeStrategy. // ValidateAuthorizeCode implements oauth2.AuthorizeCodeStrategy.
func (h *HMACCoreStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) { func (h *HMACCoreStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.Requester, token string) (err error) {
var exp = r.GetSession().GetExpiresAt(fosite.AuthorizeCode) var exp = r.GetSession().GetExpiresAt(fosite.AuthorizeCode)
if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)).Before(time.Now().UTC()) { if exp.IsZero() && r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)).Before(time.Now().UTC()) {
return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx)))) return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", r.GetRequestedAt().Add(h.Config.GetAuthorizeCodeLifespan(ctx))))
} }
@ -106,24 +108,21 @@ func (h *HMACCoreStrategy) ValidateAuthorizeCode(ctx context.Context, r fosite.R
return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", exp)) return errorsx.WithStack(fosite.ErrTokenExpired.WithHintf("Authorize code expired at '%s'.", exp))
} }
return h.Enigma.Validate(ctx, h.trimPrefix(token, "ac")) return h.Enigma.Validate(ctx, h.trimPrefix(token, tokenPrefixPartAuthorizeCode))
} }
func (h *HMACCoreStrategy) getPrefix(part string) string { func (h *HMACCoreStrategy) getPrefix(part string) string {
if h.prefix == nil { if len(h.prefix) == 0 {
prefix := "ory_%s_"
h.prefix = &prefix
} else if len(*h.prefix) == 0 {
return "" return ""
} }
return fmt.Sprintf(*h.prefix, part) return fmt.Sprintf(h.prefix, part)
}
func (h *HMACCoreStrategy) trimPrefix(token, part string) string {
return strings.TrimPrefix(token, h.getPrefix(part))
} }
func (h *HMACCoreStrategy) setPrefix(token, part string) string { func (h *HMACCoreStrategy) setPrefix(token, part string) string {
return h.getPrefix(part) + token return h.getPrefix(part) + token
} }
func (h *HMACCoreStrategy) trimPrefix(token, part string) string {
return strings.TrimPrefix(token, h.getPrefix(part))
}

View File

@ -107,6 +107,10 @@ type Client struct {
SectorIdentifier string SectorIdentifier string
Public bool Public bool
EnforcePKCE bool
EnforcePKCEChallengeMethod bool
PKCEChallengeMethod string
Audience []string Audience []string
Scopes []string Scopes []string
RedirectURIs []string RedirectURIs []string

View File

@ -0,0 +1,43 @@
package random
const (
// DefaultN is the default value of n.
DefaultN = 72
)
const (
// CharSetAlphabeticLower are literally just valid alphabetic lowercase printable ASCII chars.
CharSetAlphabeticLower = "abcdefghijklmnopqrstuvwxyz"
// CharSetAlphabeticUpper are literally just valid alphabetic uppercase printable ASCII chars.
CharSetAlphabeticUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// CharSetAlphabetic are literally just valid alphabetic printable ASCII chars.
CharSetAlphabetic = CharSetAlphabeticLower + CharSetAlphabeticUpper
// CharSetNumeric are literally just valid numeric chars.
CharSetNumeric = "0123456789"
// CharSetNumericHex are literally just valid hexadecimal printable ASCII chars.
CharSetNumericHex = CharSetNumeric + "ABCDEF"
// CharSetSymbolic are literally just valid symbolic printable ASCII chars.
CharSetSymbolic = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
// CharSetSymbolicRFC3986Unreserved are RFC3986 unreserved symbol characters.
// See https://www.rfc-editor.org/rfc/rfc3986#section-2.3.
CharSetSymbolicRFC3986Unreserved = "-._~"
// CharSetAlphaNumeric are literally just valid alphanumeric printable ASCII chars.
CharSetAlphaNumeric = CharSetAlphabetic + CharSetNumeric
// CharSetASCII are literally just valid printable ASCII chars.
CharSetASCII = CharSetAlphabetic + CharSetNumeric + CharSetSymbolic
// CharSetRFC3986Unreserved are RFC3986 unreserved characters.
// See https://www.rfc-editor.org/rfc/rfc3986#section-2.3.
CharSetRFC3986Unreserved = CharSetAlphabetic + CharSetNumeric + CharSetSymbolicRFC3986Unreserved
// CharSetUnambiguousUpper are a set of unambiguous uppercase characters.
CharSetUnambiguousUpper = "ABCDEFGHJKLMNOPQRTUVWYXZ2346789"
)

View File

@ -0,0 +1,136 @@
package random
import (
"crypto/rand"
"fmt"
"io"
"math/big"
)
// Cryptographical is the production random.Provider which uses crypto/rand.
type Cryptographical struct{}
// Read implements the io.Reader interface.
func (r *Cryptographical) Read(p []byte) (n int, err error) {
return io.ReadFull(rand.Reader, p)
}
// BytesErr returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function returns it.
func (r *Cryptographical) BytesErr() (data []byte, err error) {
data = make([]byte, DefaultN)
_, err = rand.Read(data)
return data, err
}
// Bytes returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function ignores it.
func (r *Cryptographical) Bytes() (data []byte) {
data, _ = r.BytesErr()
return data
}
// BytesCustomErr returns random data as bytes with n length and can contain only byte values from the provided
// values. If n is less than 1 then DefaultN is used instead. If an error is returned from the random read this function
// returns it.
func (r *Cryptographical) BytesCustomErr(n int, charset []byte) (data []byte, err error) {
if n < 1 {
n = DefaultN
}
data = make([]byte, n)
if _, err = rand.Read(data); err != nil {
return nil, err
}
t := len(charset)
for i := 0; i < n; i++ {
data[i] = charset[data[i]%byte(t)]
}
return data, nil
}
// StringCustomErr is an overload of BytesCustomWithErr which takes a characters string and returns a string.
func (r *Cryptographical) StringCustomErr(n int, characters string) (data string, err error) {
var d []byte
if d, err = r.BytesCustomErr(n, []byte(characters)); err != nil {
return "", err
}
return string(d), nil
}
// BytesCustom returns random data as bytes with n length and can contain only byte values from the provided values.
// If n is less than 1 then DefaultN is used instead. If an error is returned from the random read this function
// ignores it.
func (r *Cryptographical) BytesCustom(n int, charset []byte) (data []byte) {
data, _ = r.BytesCustomErr(n, charset)
return data
}
// StringCustom is an overload of BytesCustom which takes a characters string and returns a string.
func (r *Cryptographical) StringCustom(n int, characters string) (data string) {
return string(r.BytesCustom(n, []byte(characters)))
}
// IntErr returns a random *big.Int error combination with a maximum of max.
func (r *Cryptographical) IntErr(max *big.Int) (value *big.Int, err error) {
if max == nil {
return nil, fmt.Errorf("max is required")
}
if max.Sign() <= 0 {
return nil, fmt.Errorf("max must be 1 or more")
}
return rand.Int(rand.Reader, max)
}
// Int returns a random *big.Int with a maximum of max.
func (r *Cryptographical) Int(max *big.Int) (value *big.Int) {
var err error
if value, err = r.IntErr(max); err != nil {
return big.NewInt(-1)
}
return value
}
// IntegerErr returns a random int error combination with a maximum of n.
func (r *Cryptographical) IntegerErr(n int) (value int, err error) {
if n <= 0 {
return 0, fmt.Errorf("n must be more than 0")
}
max := big.NewInt(int64(n))
var result *big.Int
if result, err = r.IntErr(max); err != nil {
return 0, err
}
value = int(result.Int64())
if value < 0 {
return 0, fmt.Errorf("generated number is too big for int")
}
return value, nil
}
// Integer returns a random int with a maximum of n.
func (r *Cryptographical) Integer(n int) (value int) {
value, _ = r.IntegerErr(n)
return value
}

View File

@ -0,0 +1,126 @@
package random
import (
"fmt"
"math/big"
"math/rand"
"time"
)
// NewMathematical runs rand.Seed with the current time and returns a random.Provider, specifically *random.Mathematical.
func NewMathematical() *Mathematical {
rand.Seed(time.Now().UnixNano())
return &Mathematical{}
}
// Mathematical is the random.Provider which uses math/rand and is COMPLETELY UNSAFE FOR PRODUCTION IN MOST SITUATIONS.
// Use random.Cryptographical instead.
type Mathematical struct{}
// Read implements the io.Reader interface.
func (r *Mathematical) Read(p []byte) (n int, err error) {
return rand.Read(p) //nolint:gosec
}
// BytesErr returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function returns it.
func (r *Mathematical) BytesErr() (data []byte, err error) {
data = make([]byte, DefaultN)
if _, err = rand.Read(data); err != nil { //nolint:gosec
return nil, err
}
return data, nil
}
// Bytes returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function ignores it.
func (r *Mathematical) Bytes() (data []byte) {
data, _ = r.BytesErr()
return data
}
// BytesCustomErr returns random data as bytes with n length and can contain only byte values from the provided
// values. If n is less than 1 then DefaultN is used instead. If an error is returned from the random read this function
// returns it.
func (r *Mathematical) BytesCustomErr(n int, charset []byte) (data []byte, err error) {
if n < 1 {
n = DefaultN
}
data = make([]byte, n)
if _, err = rand.Read(data); err != nil { //nolint:gosec
return nil, err
}
t := len(charset)
for i := 0; i < n; i++ {
data[i] = charset[data[i]%byte(t)]
}
return data, nil
}
// StringCustomErr is an overload of BytesCustomWithErr which takes a characters string and returns a string.
func (r *Mathematical) StringCustomErr(n int, characters string) (data string, err error) {
var d []byte
if d, err = r.BytesCustomErr(n, []byte(characters)); err != nil {
return "", err
}
return string(d), nil
}
// BytesCustom returns random data as bytes with n length and can contain only byte values from the provided values.
// If n is less than 1 then DefaultN is used instead. If an error is returned from the random read this function
// ignores it.
func (r *Mathematical) BytesCustom(n int, charset []byte) (data []byte) {
data, _ = r.BytesCustomErr(n, charset)
return data
}
// StringCustom is an overload of BytesCustom which takes a characters string and returns a string.
func (r *Mathematical) StringCustom(n int, characters string) (data string) {
return string(r.BytesCustom(n, []byte(characters)))
}
// IntErr returns a random *big.Int error combination with a maximum of max.
func (r *Mathematical) IntErr(max *big.Int) (value *big.Int, err error) {
if max == nil {
return nil, fmt.Errorf("max is required")
}
if max.Sign() <= 0 {
return nil, fmt.Errorf("max must be 1 or more")
}
return big.NewInt(int64(rand.Intn(max.Sign()))), nil //nolint:gosec
}
// Int returns a random *big.Int with a maximum of max.
func (r *Mathematical) Int(max *big.Int) (value *big.Int) {
var err error
if value, err = r.IntErr(max); err != nil {
return big.NewInt(-1)
}
return value
}
// IntegerErr returns a random int error combination with a maximum of n.
func (r *Mathematical) IntegerErr(n int) (output int, err error) {
return r.Integer(n), nil
}
// Integer returns a random int with a maximum of n.
func (r *Mathematical) Integer(n int) int {
return rand.Intn(n) //nolint:gosec
}

View File

@ -0,0 +1,46 @@
package random
import (
"io"
"math/big"
)
// Provider of random functions and functionality.
type Provider interface {
io.Reader
// BytesErr returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function returns it.
BytesErr() (data []byte, err error)
// Bytes returns random data as bytes with the standard random.DefaultN length and can contain any byte values
// (including unreadable byte values). If an error is returned from the random read this function ignores it.
Bytes() (data []byte)
// BytesCustomErr returns random data as bytes with n length and can contain only byte values from the provided
// values. If n is less than 1 then DefaultN is used instead. If an error is returned from the random read this function
// returns it.
BytesCustomErr(n int, charset []byte) (data []byte, err error)
// StringCustomErr is an overload of BytesCustomWithErr which takes a characters string and returns a string.
StringCustomErr(n int, characters string) (data string, err error)
// BytesCustom returns random data as bytes with n length and can contain only byte values from the provided
// values. If n is less than 1 then DefaultN is used instead.
BytesCustom(n int, charset []byte) (data []byte)
// StringCustom is an overload of GenerateCustom which takes a characters string and returns a string.
StringCustom(n int, characters string) (data string)
// IntErr returns a random *big.Int error combination with a maximum of max.
IntErr(max *big.Int) (value *big.Int, err error)
// Int returns a random *big.Int with a maximum of max.
Int(max *big.Int) (value *big.Int)
// IntegerErr returns a random int error combination with a maximum of n.
IntegerErr(n int) (value int, err error)
// Integer returns a random integer with a maximum of n.
Integer(n int) (value int)
}

View File

@ -6,14 +6,12 @@ import (
const ( const (
assetsRoot = "public_html" assetsRoot = "public_html"
assetsSwagger = assetsRoot + "/api"
fileOpenAPI = "openapi.yml"
fileIndexHTML = "index.html"
fileLogo = "logo.png" fileLogo = "logo.png"
extHTML = ".html" extHTML = ".html"
extJSON = ".json" extJSON = ".json"
extYML = ".yml"
) )
var ( var (
@ -52,8 +50,8 @@ var (
const ( const (
environment = "ENVIRONMENT" environment = "ENVIRONMENT"
dev = "dev" dev = "dev"
f = "false" strFalse = "false"
t = "true" strTrue = "true"
localhost = "localhost" localhost = "localhost"
schemeHTTP = "http" schemeHTTP = "http"
schemeHTTPS = "https" schemeHTTPS = "https"
@ -76,7 +74,8 @@ X_AUTHELIA_HEALTHCHECK_PATH=%s
` `
const ( const (
tmplCSPSwagger = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'; base-uri 'self'" tmplCSPSwaggerNonce = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline' 'nonce-%s'; style-src 'self' 'nonce-%s'; base-uri 'self'"
tmplCSPSwagger = "default-src 'self'; img-src 'self' https://validator.swagger.io data:; object-src 'none'; script-src 'self' 'unsafe-inline'; style-src 'self'; base-uri 'self'"
) )
const ( const (

View File

@ -93,9 +93,9 @@ func handleNotFound(next fasthttp.RequestHandler) fasthttp.RequestHandler {
func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler { func handleRouter(config schema.Configuration, providers middlewares.Providers) fasthttp.RequestHandler {
optsTemplatedFile := NewTemplatedFileOptions(&config) optsTemplatedFile := NewTemplatedFileOptions(&config)
serveIndexHandler := ServeTemplatedFile(assetsRoot, fileIndexHTML, optsTemplatedFile) serveIndexHandler := ServeTemplatedFile(providers.Templates.GetAssetIndexTemplate(), optsTemplatedFile)
serveSwaggerHandler := ServeTemplatedFile(assetsSwagger, fileIndexHTML, optsTemplatedFile) serveOpenAPIHandler := ServeTemplatedOpenAPI(providers.Templates.GetAssetOpenAPIIndexTemplate(), optsTemplatedFile)
serveSwaggerAPIHandler := ServeTemplatedFile(assetsSwagger, fileOpenAPI, optsTemplatedFile) serveOpenAPISpecHandler := ETagRootURL(ServeTemplatedOpenAPI(providers.Templates.GetAssetOpenAPISpecTemplate(), optsTemplatedFile))
handlerPublicHTML := newPublicHTMLEmbeddedHandler() handlerPublicHTML := newPublicHTMLEmbeddedHandler()
handlerLocales := newLocalesEmbeddedHandler() handlerLocales := newLocalesEmbeddedHandler()
@ -126,10 +126,12 @@ func handleRouter(config schema.Configuration, providers middlewares.Providers)
r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverride(config.Server.AssetPath, 0, handlerLocales)) r.GET("/locales/{language:[a-z]{1,3}}/{namespace:[a-z]+}.json", middlewares.AssetOverride(config.Server.AssetPath, 0, handlerLocales))
// Swagger. // Swagger.
r.GET("/api/", middleware(serveSwaggerHandler)) r.GET("/api/", middleware(serveOpenAPIHandler))
r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS) r.OPTIONS("/api/", policyCORSPublicGET.HandleOPTIONS)
r.GET("/api/"+fileOpenAPI, policyCORSPublicGET.Middleware(middleware(serveSwaggerAPIHandler))) r.GET("/api/index.html", middleware(serveOpenAPIHandler))
r.OPTIONS("/api/"+fileOpenAPI, policyCORSPublicGET.HandleOPTIONS) r.OPTIONS("/api/index.html", policyCORSPublicGET.HandleOPTIONS)
r.GET("/api/openapi.yml", policyCORSPublicGET.Middleware(middleware(serveOpenAPISpecHandler)))
r.OPTIONS("/api/openapi.yml", policyCORSPublicGET.HandleOPTIONS)
for _, file := range filesSwagger { for _, file := range filesSwagger {
r.GET("/api/"+file, handlerPublicHTML) r.GET("/api/"+file, handlerPublicHTML)

View File

@ -19,6 +19,10 @@ import (
// CreateDefaultServer Create Authelia's internal webserver with the given configuration and providers. // CreateDefaultServer Create Authelia's internal webserver with the given configuration and providers.
func CreateDefaultServer(config schema.Configuration, providers middlewares.Providers) (server *fasthttp.Server, listener net.Listener, err error) { func CreateDefaultServer(config schema.Configuration, providers middlewares.Providers) (server *fasthttp.Server, listener net.Listener, err error) {
if err = providers.Templates.LoadTemplatedAssets(assets); err != nil {
return nil, nil, fmt.Errorf("failed to load templated assets: %w", err)
}
server = &fasthttp.Server{ server = &fasthttp.Server{
ErrorHandler: handleError(), ErrorHandler: handleError(),
Handler: handleRouter(config, providers), Handler: handleRouter(config, providers),

View File

@ -21,6 +21,7 @@ import (
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging" "github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/templates"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/utils"
) )
@ -134,10 +135,17 @@ type TLSServerContext struct {
port int port int
} }
func NewTLSServerContext(configuration schema.Configuration) (*TLSServerContext, error) { func NewTLSServerContext(configuration schema.Configuration) (serverContext *TLSServerContext, err error) {
serverContext := new(TLSServerContext) serverContext = new(TLSServerContext)
s, listener, err := CreateDefaultServer(configuration, middlewares.Providers{}) providers := middlewares.Providers{}
providers.Templates, err = templates.New(templates.Config{EmailTemplatesPath: configuration.Notifier.TemplatePath})
if err != nil {
return nil, err
}
s, listener, err := CreateDefaultServer(configuration, providers)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -1,56 +1,43 @@
package server package server
import ( import (
"bytes"
"crypto/sha1" //nolint:gosec
"encoding/hex"
"fmt" "fmt"
"io"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"text/template" "sync"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
"github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/configuration/schema"
"github.com/authelia/authelia/v4/internal/logging"
"github.com/authelia/authelia/v4/internal/middlewares" "github.com/authelia/authelia/v4/internal/middlewares"
"github.com/authelia/authelia/v4/internal/utils" "github.com/authelia/authelia/v4/internal/random"
"github.com/authelia/authelia/v4/internal/templates"
) )
// ServeTemplatedFile serves a templated version of a specified file, // ServeTemplatedFile serves a templated version of a specified file,
// this is utilised to pass information between the backend and frontend // this is utilised to pass information between the backend and frontend
// and generate a nonce to support a restrictive CSP while using material-ui. // and generate a nonce to support a restrictive CSP while using material-ui.
func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) middlewares.RequestHandler { func ServeTemplatedFile(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
logger := logging.Logger()
a, err := assets.Open(path.Join(publicDir, file))
if err != nil {
logger.Fatalf("Unable to open %s: %s", file, err)
}
b, err := io.ReadAll(a)
if err != nil {
logger.Fatalf("Unable to read %s: %s", file, err)
}
tmpl, err := template.New("file").Parse(string(b))
if err != nil {
logger.Fatalf("Unable to parse %s template: %s", file, err)
}
isDevEnvironment := os.Getenv(environment) == dev isDevEnvironment := os.Getenv(environment) == dev
ext := filepath.Ext(t.Name())
return func(ctx *middlewares.AutheliaCtx) { return func(ctx *middlewares.AutheliaCtx) {
logoOverride := f var err error
logoOverride := strFalse
if opts.AssetPath != "" { if opts.AssetPath != "" {
if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil { if _, err = os.Stat(filepath.Join(opts.AssetPath, fileLogo)); err == nil {
logoOverride = t logoOverride = strTrue
} }
} }
switch extension := filepath.Ext(file); extension { switch ext {
case extHTML: case extHTML:
ctx.SetContentTypeTextHTML() ctx.SetContentTypeTextHTML()
case extJSON: case extJSON:
@ -59,11 +46,9 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd
ctx.SetContentTypeTextPlain() ctx.SetContentTypeTextPlain()
} }
nonce := utils.RandomString(32, utils.CharSetAlphaNumeric) nonce := ctx.Providers.Random.StringCustom(32, random.CharSetAlphaNumeric)
switch { switch {
case publicDir == assetsSwagger:
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwagger, nonce, nonce))
case ctx.Configuration.Server.Headers.CSPTemplate != "": case ctx.Configuration.Server.Headers.CSPTemplate != "":
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce)) ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, strings.ReplaceAll(ctx.Configuration.Server.Headers.CSPTemplate, placeholderCSPNonce, nonce))
case isDevEnvironment: case isDevEnvironment:
@ -72,15 +57,99 @@ func ServeTemplatedFile(publicDir, file string, opts *TemplatedFileOptions) midd
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce)) ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPDefault, nonce))
} }
if err = tmpl.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil { if err = t.Execute(ctx.Response.BodyWriter(), opts.CommonData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce, logoOverride)); err != nil {
ctx.RequestCtx.Error("an error occurred", 503) ctx.RequestCtx.Error("an error occurred", 503)
logger.Errorf("Unable to execute template: %v", err) ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
return return
} }
} }
} }
// ServeTemplatedOpenAPI serves templated OpenAPI related files.
func ServeTemplatedOpenAPI(t templates.Template, opts *TemplatedFileOptions) middlewares.RequestHandler {
ext := filepath.Ext(t.Name())
spec := ext == extYML
return func(ctx *middlewares.AutheliaCtx) {
var nonce string
if spec {
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, tmplCSPSwagger)
} else {
nonce = ctx.Providers.Random.StringCustom(32, random.CharSetAlphaNumeric)
ctx.Response.Header.Add(fasthttp.HeaderContentSecurityPolicy, fmt.Sprintf(tmplCSPSwaggerNonce, nonce, nonce))
}
switch ext {
case extHTML:
ctx.SetContentTypeTextHTML()
case extYML:
ctx.SetContentTypeApplicationYAML()
default:
ctx.SetContentTypeTextPlain()
}
var err error
if err = t.Execute(ctx.Response.BodyWriter(), opts.OpenAPIData(ctx.BasePath(), ctx.RootURLSlash().String(), nonce)); err != nil {
ctx.RequestCtx.Error("an error occurred", 503)
ctx.Logger.WithError(err).Errorf("Error occcurred rendering template")
return
}
}
}
// ETagRootURL dynamically matches the If-None-Match header and adds the ETag header.
func ETagRootURL(next middlewares.RequestHandler) middlewares.RequestHandler {
etags := map[string][]byte{}
h := sha1.New() //nolint:gosec // Usage is for collision avoidance not security.
mu := &sync.Mutex{}
return func(ctx *middlewares.AutheliaCtx) {
k := ctx.RootURLSlash().String()
mu.Lock()
etag, ok := etags[k]
mu.Unlock()
if ok && bytes.Equal(etag, ctx.Request.Header.PeekBytes(headerIfNoneMatch)) {
ctx.Response.Header.SetBytesKV(headerETag, etag)
ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
ctx.SetStatusCode(fasthttp.StatusNotModified)
return
}
next(ctx)
mu.Lock()
h.Write(ctx.Response.Body())
sum := h.Sum(nil)
h.Reset()
etagNew := make([]byte, hex.EncodedLen(len(sum)))
hex.Encode(etagNew, sum)
if !ok || !bytes.Equal(etag, etagNew) {
etags[k] = etagNew
}
mu.Unlock()
ctx.Response.Header.SetBytesKV(headerETag, etagNew)
ctx.Response.Header.SetBytesKV(headerCacheControl, headerValueCacheControlETaggedAssets)
}
}
func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (err error) { func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (err error) {
if disabled { if disabled {
return nil return nil
@ -120,11 +189,17 @@ func writeHealthCheckEnv(disabled bool, scheme, host, path string, port int) (er
func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) { func NewTemplatedFileOptions(config *schema.Configuration) (opts *TemplatedFileOptions) {
opts = &TemplatedFileOptions{ opts = &TemplatedFileOptions{
AssetPath: config.Server.AssetPath, AssetPath: config.Server.AssetPath,
DuoSelfEnrollment: f, DuoSelfEnrollment: strFalse,
RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled), RememberMe: strconv.FormatBool(config.Session.RememberMeDuration != schema.RememberMeDisabled),
ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable), ResetPassword: strconv.FormatBool(!config.AuthenticationBackend.PasswordReset.Disable),
ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(), ResetPasswordCustomURL: config.AuthenticationBackend.PasswordReset.CustomURL.String(),
Theme: config.Theme, Theme: config.Theme,
EndpointsPasswordReset: !(config.AuthenticationBackend.PasswordReset.Disable || config.AuthenticationBackend.PasswordReset.CustomURL.String() != ""),
EndpointsWebauthn: !config.Webauthn.Disable,
EndpointsTOTP: !config.TOTP.Disable,
EndpointsDuo: !config.DuoAPI.Disable,
EndpointsOpenIDConnect: !(config.IdentityProviders.OIDC == nil),
} }
if !config.DuoAPI.Disable { if !config.DuoAPI.Disable {
@ -143,6 +218,12 @@ type TemplatedFileOptions struct {
ResetPasswordCustomURL string ResetPasswordCustomURL string
Session string Session string
Theme string Theme string
EndpointsPasswordReset bool
EndpointsWebauthn bool
EndpointsTOTP bool
EndpointsDuo bool
EndpointsOpenIDConnect bool
} }
// CommonData returns a TemplatedFileCommonData with the dynamic options. // CommonData returns a TemplatedFileCommonData with the dynamic options.
@ -161,6 +242,22 @@ func (options *TemplatedFileOptions) CommonData(base, baseURL, nonce, logoOverri
} }
} }
// OpenAPIData returns a TemplatedFileOpenAPIData with the dynamic options.
func (options *TemplatedFileOptions) OpenAPIData(base, baseURL, nonce string) TemplatedFileOpenAPIData {
return TemplatedFileOpenAPIData{
Base: base,
BaseURL: baseURL,
CSPNonce: nonce,
Session: options.Session,
PasswordReset: options.EndpointsPasswordReset,
Webauthn: options.EndpointsWebauthn,
TOTP: options.EndpointsTOTP,
Duo: options.EndpointsDuo,
OpenIDConnect: options.EndpointsOpenIDConnect,
}
}
// TemplatedFileCommonData is a struct which is used for many templated files. // TemplatedFileCommonData is a struct which is used for many templated files.
type TemplatedFileCommonData struct { type TemplatedFileCommonData struct {
Base string Base string
@ -174,3 +271,16 @@ type TemplatedFileCommonData struct {
Session string Session string
Theme string Theme string
} }
// TemplatedFileOpenAPIData is a struct which is used for the OpenAPI spec file.
type TemplatedFileOpenAPIData struct {
Base string
BaseURL string
CSPNonce string
Session string
PasswordReset bool
Webauthn bool
TOTP bool
Duo bool
OpenIDConnect bool
}

View File

@ -9,7 +9,7 @@ import (
"testing" "testing"
"time" "time"
mapset "github.com/deckarep/golang-set" mapset "github.com/deckarep/golang-set/v2"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
) )
@ -89,7 +89,7 @@ func (s *CustomHeadersScenario) TestShouldForwardCustomHeaderForAuthenticatedUse
s.collectScreenshot(ctx.Err(), s.Page) s.collectScreenshot(ctx.Err(), s.Page)
}() }()
expectedGroups := mapset.NewSetWith("dev", "admins") expectedGroups := mapset.NewSet("dev", "admins")
targetURL := fmt.Sprintf("%s/headers", PublicBaseURL) targetURL := fmt.Sprintf("%s/headers", PublicBaseURL)
s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, targetURL) s.doLoginOneFactor(s.T(), s.Context(ctx), "john", "password", false, targetURL)
@ -109,7 +109,7 @@ func (s *CustomHeadersScenario) TestShouldForwardCustomHeaderForAuthenticatedUse
} }
groups := strings.Split(payload.Headers.ForwardedGroups, ",") groups := strings.Split(payload.Headers.ForwardedGroups, ",")
actualGroups := mapset.NewSet() actualGroups := mapset.NewSet[string]()
for _, group := range groups { for _, group := range groups {
actualGroups.Add(group) actualGroups.Add(group)

View File

@ -792,7 +792,7 @@ func (s *CLISuite) TestStorage00ShouldShowCorrectPreInitInformation() {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")
s.Assert().Contains(output, "Error: command requires the use of a up to date schema version: storage schema outdated: version 0 is outdated please migrate to version 7 in order to use this command or use an older binary\n") s.Assert().Regexp(regexp.MustCompile(`^Error: command requires the use of a up to date schema version: storage schema outdated: version 0 is outdated please migrate to version \d+ in order to use this command or use an older binary\n`), output)
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "migrate", "down", "--target=0", "--destroy-data", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")
@ -1131,7 +1131,14 @@ func (s *CLISuite) TestStorage05ShouldChangeEncryptionKey() {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Encryption Key Validation: FAILURE\n\n\tCause: the configured encryption key does not appear to be valid for this database which may occur if the encryption key was changed in the configuration without using the cli to change it in the database.\n\nTables:\n\n\tTable (oauth2_access_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_authorization_code_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (totp_configurations): FAILURE\n\t\tInvalid Rows: 4\n\t\tTotal Rows: 4\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "Storage Encryption Key Validation: FAILURE\n\n\tCause: the configured encryption key does not appear to be valid for this database which may occur if the encryption key was changed in the configuration without using the cli to change it in the database.\n\nTables:\n\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_access_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_authorization_code_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (totp_configurations): FAILURE\n\t\tInvalid Rows: 4\n\t\tTotal Rows: 4\n")
s.Assert().Contains(output, "\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
@ -1141,7 +1148,14 @@ func (s *CLISuite) TestStorage05ShouldChangeEncryptionKey() {
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "check", "--verbose", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().NoError(err) s.Assert().NoError(err)
s.Assert().Contains(output, "Storage Encryption Key Validation: SUCCESS\n\nTables:\n\n\tTable (oauth2_access_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_authorization_code_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n\n\tTable (totp_configurations): SUCCESS\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 4\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n") s.Assert().Contains(output, "Storage Encryption Key Validation: SUCCESS\n\nTables:\n\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_access_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_authorization_code_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_openid_connect_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_pkce_request_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (oauth2_refresh_token_session): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
s.Assert().Contains(output, "\n\n\tTable (totp_configurations): SUCCESS\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 4\n")
s.Assert().Contains(output, "\n\n\tTable (webauthn_devices): N/A\n\t\tInvalid Rows: 0\n\t\tTotal Rows: 0\n")
output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"}) output, err = s.Exec("authelia-backend", []string{"authelia", s.testArg, s.coverageArg, "storage", "encryption", "change-key", "--encryption-key=apple-apple-apple-apple", "--config=/config/configuration.storage.yml"})
s.Assert().EqualError(err, "exit status 1") s.Assert().EqualError(err, "exit status 1")

View File

@ -8,7 +8,6 @@ const (
// Template File Names. // Template File Names.
const ( const (
TemplateNameEmailIdentityVerification = "IdentityVerification" TemplateNameEmailIdentityVerification = "IdentityVerification"
TemplateNameEmailPasswordReset = "PasswordReset"
TemplateNameEmailEvent = "Event" TemplateNameEmailEvent = "Event"
) )

View File

@ -10,6 +10,8 @@ import (
"fmt" "fmt"
"hash" "hash"
"os" "os"
"path"
"path/filepath"
"reflect" "reflect"
"sort" "sort"
"strconv" "strconv"
@ -49,6 +51,28 @@ func FuncMap() map[string]any {
"b64dec": FuncB64Dec, "b64dec": FuncB64Dec,
"b32enc": FuncB32Enc, "b32enc": FuncB32Enc,
"b32dec": FuncB32Dec, "b32dec": FuncB32Dec,
"list": FuncList,
"dict": FuncDict,
"get": FuncGet,
"set": FuncSet,
"isAbs": path.IsAbs,
"base": path.Base,
"dir": path.Dir,
"ext": path.Ext,
"clean": path.Clean,
"osBase": filepath.Base,
"osClean": filepath.Clean,
"osDir": filepath.Dir,
"osExt": filepath.Ext,
"osIsAbs": filepath.IsAbs,
"deepEqual": reflect.DeepEqual,
"typeOf": FuncTypeOf,
"typeIs": FuncTypeIs,
"typeIsLike": FuncTypeIsLike,
"kindOf": FuncKindOf,
"kindIs": FuncKindIs,
"default": FuncDefault,
"empty": FuncEmpty,
} }
} }
@ -202,58 +226,6 @@ func FuncStringQuote(in ...any) string {
return strings.Join(out, " ") return strings.Join(out, " ")
} }
func strval(v interface{}) string {
switch v := v.(type) {
case string:
return v
case []byte:
return string(v)
case fmt.Stringer:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func strslice(v any) []string {
switch v := v.(type) {
case []string:
return v
case []interface{}:
b := make([]string, 0, len(v))
for _, s := range v {
if s != nil {
b = append(b, strval(s))
}
}
return b
default:
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Array, reflect.Slice:
l := val.Len()
b := make([]string, 0, l)
for i := 0; i < l; i++ {
value := val.Index(i).Interface()
if value != nil {
b = append(b, strval(value))
}
}
return b
default:
if v == nil {
return []string{}
}
return []string{strval(v)}
}
}
}
// FuncIterate is a template function which takes a single uint returning a slice of units from 0 up to that number. // FuncIterate is a template function which takes a single uint returning a slice of units from 0 up to that number.
func FuncIterate(count *uint) (out []uint) { func FuncIterate(count *uint) (out []uint) {
var i uint var i uint
@ -308,3 +280,107 @@ func FuncStringJoinX(elems []string, sep string, n int, p string) string {
return buf.String() return buf.String()
} }
// FuncTypeIs is a helper function that provides similar functionality to the helm typeIs func.
func FuncTypeIs(is string, v any) bool {
return is == FuncTypeOf(v)
}
// FuncTypeIsLike is a helper function that provides similar functionality to the helm typeIsLike func.
func FuncTypeIsLike(is string, v any) bool {
t := FuncTypeOf(v)
return is == t || "*"+is == t
}
// FuncTypeOf is a helper function that provides similar functionality to the helm typeOf func.
func FuncTypeOf(v any) string {
return reflect.ValueOf(v).Type().String()
}
// FuncKindIs is a helper function that provides similar functionality to the helm kindIs func.
func FuncKindIs(is string, v any) bool {
return is == FuncKindOf(v)
}
// FuncKindOf is a helper function that provides similar functionality to the helm kindOf func.
func FuncKindOf(v any) string {
return reflect.ValueOf(v).Kind().String()
}
// FuncList is a helper function that provides similar functionality to the helm list func.
func FuncList(items ...any) []any {
return items
}
// FuncDict is a helper function that provides similar functionality to the helm dict func.
func FuncDict(pairs ...any) map[string]any {
m := map[string]any{}
p := len(pairs)
for i := 0; i < p; i += 2 {
key := strval(pairs[i])
if i+1 >= p {
m[key] = ""
continue
}
m[key] = pairs[i+1]
}
return m
}
// FuncGet is a helper function that provides similar functionality to the helm get func.
func FuncGet(m map[string]any, key string) any {
if val, ok := m[key]; ok {
return val
}
return ""
}
// FuncSet is a helper function that provides similar functionality to the helm set func.
func FuncSet(m map[string]any, key string, value any) map[string]any {
m[key] = value
return m
}
// FuncDefault is a helper function that provides similar functionality to the helm default func.
func FuncDefault(d any, vals ...any) any {
if FuncEmpty(vals) || FuncEmpty(vals[0]) {
return d
}
return vals[0]
}
// FuncEmpty is a helper function that provides similar functionality to the helm empty func.
func FuncEmpty(v any) bool {
rv := reflect.ValueOf(v)
if !rv.IsValid() {
return true
}
switch rv.Kind() {
default:
return rv.IsNil()
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return rv.Len() == 0
case reflect.Bool:
return !rv.Bool()
case reflect.Complex64, reflect.Complex128:
return rv.Complex() == 0
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return rv.Int() == 0
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
return rv.Uint() == 0
case reflect.Float32, reflect.Float64:
return rv.Float() == 0
case reflect.Struct:
return false
}
}

View File

@ -357,6 +357,7 @@ func TestFuncSortAlpha(t *testing.T) {
}{ }{
{"ShouldSortStrings", []string{"a", "c", "b"}, []string{"a", "b", "c"}}, {"ShouldSortStrings", []string{"a", "c", "b"}, []string{"a", "b", "c"}},
{"ShouldSortIntegers", []int{2, 3, 1}, []string{"1", "2", "3"}}, {"ShouldSortIntegers", []int{2, 3, 1}, []string{"1", "2", "3"}},
{"ShouldSortSingleValue", 1, []string{"1"}},
} }
for _, tc := range testCases { for _, tc := range testCases {
@ -469,3 +470,171 @@ func TestFuncStringSQuote(t *testing.T) {
}) })
} }
} }
func TestFuncTypeOf(t *testing.T) {
astring := "typeOfExample"
anint := 5
astringslice := []string{astring}
anintslice := []int{anint}
testCases := []struct {
name string
have any
expected string
expectedKind string
}{
{"String", astring, "string", "string"},
{"StringPtr", &astring, "*string", "ptr"},
{"StringSlice", astringslice, "[]string", "slice"},
{"StringSlicePtr", &astringslice, "*[]string", "ptr"},
{"Integer", anint, "int", "int"},
{"IntegerPtr", &anint, "*int", "ptr"},
{"IntegerSlice", anintslice, "[]int", "slice"},
{"IntegerSlicePtr", &anintslice, "*[]int", "ptr"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncTypeOf(tc.have))
assert.Equal(t, tc.expectedKind, FuncKindOf(tc.have))
})
}
}
func TestFuncTypeIs(t *testing.T) {
astring := "typeIsExample"
anint := 10
astringslice := []string{astring}
anintslice := []int{anint}
testCases := []struct {
name string
is string
have any
expected bool
expectedLike bool
expectedKind bool
}{
{"ShouldMatchStringAsString", "string", astring, true, true, true},
{"ShouldMatchStringPtrAsString", "string", &astring, false, true, false},
{"ShouldNotMatchStringAsInt", "int", astring, false, false, false},
{"ShouldNotMatchStringSliceAsStringSlice", "[]string", astringslice, true, true, false},
{"ShouldNotMatchStringSlicePtrAsStringSlice", "[]string", &astringslice, false, true, false},
{"ShouldNotMatchStringSlicePtrAsStringSlicePtr", "*[]string", &astringslice, true, true, false},
{"ShouldNotMatchStringSliceAsString", "string", astringslice, false, false, false},
{"ShouldMatchIntAsInt", "int", anint, true, true, true},
{"ShouldMatchIntPtrAsInt", "int", &anint, false, true, false},
{"ShouldNotMatchIntAsString", "string", anint, false, false, false},
{"ShouldMatchIntegerSliceAsIntSlice", "[]int", anintslice, true, true, false},
{"ShouldMatchIntegerSlicePtrAsIntSlice", "[]int", &anintslice, false, true, false},
{"ShouldMatchIntegerSlicePtrAsIntSlicePtr", "*[]int", &anintslice, true, true, false},
{"ShouldNotMatchIntegerSliceAsInt", "int", anintslice, false, false, false},
{"ShouldMatchKindSlice", "slice", anintslice, false, false, true},
{"ShouldMatchKindPtr", "ptr", &anintslice, false, false, true},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncTypeIs(tc.is, tc.have))
assert.Equal(t, tc.expectedLike, FuncTypeIsLike(tc.is, tc.have))
assert.Equal(t, tc.expectedKind, FuncKindIs(tc.is, tc.have))
})
}
}
func TestFuncList(t *testing.T) {
assert.Equal(t, []any{"a", "b", "c"}, FuncList("a", "b", "c"))
assert.Equal(t, []any{1, 2, 3}, FuncList(1, 2, 3))
}
func TestFuncDict(t *testing.T) {
assert.Equal(t, map[string]any{"a": 1}, FuncDict("a", 1))
assert.Equal(t, map[string]any{"a": 1, "b": ""}, FuncDict("a", 1, "b"))
assert.Equal(t, map[string]any{"1": 1, "b": 2}, FuncDict(1, 1, "b", 2))
assert.Equal(t, map[string]any{"true": 1, "b": 2}, FuncDict(true, 1, "b", 2))
assert.Equal(t, map[string]any{"a": 2, "b": 3}, FuncDict("a", 1, "a", 2, "b", 3))
}
func TestFuncGet(t *testing.T) {
assert.Equal(t, 123, FuncGet(map[string]any{"abc": 123}, "abc"))
assert.Equal(t, "", FuncGet(map[string]any{"abc": 123}, "123"))
}
func TestFuncSet(t *testing.T) {
assert.Equal(t, map[string]any{"abc": 123, "123": true}, FuncSet(map[string]any{"abc": 123}, "123", true))
assert.Equal(t, map[string]any{"abc": true}, FuncSet(map[string]any{"abc": 123}, "abc", true))
}
func TestFuncDefault(t *testing.T) {
testCases := []struct {
name string
value []any
have any
expected any
}{
{"ShouldDefaultEmptyString", []any{""}, "default", "default"},
{"ShouldNotDefaultString", []any{"not default"}, "default", "not default"},
{"ShouldDefaultEmptyInteger", []any{0}, 1, 1},
{"ShouldNotDefaultInteger", []any{20}, 1, 20},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncDefault(tc.have, tc.value...))
})
}
}
func TestFuncEmpty(t *testing.T) {
var nilv *string
testCases := []struct {
name string
value any
expected bool
}{
{"ShouldBeEmptyNil", nilv, true},
{"ShouldBeEmptyNilNil", nil, true},
{"ShouldBeEmptyString", "", true},
{"ShouldNotBeEmptyString", "abc", false},
{"ShouldBeEmptyArray", []string{}, true},
{"ShouldNotBeEmptyArray", []string{"abc"}, false},
{"ShouldBeEmptyInteger", 0, true},
{"ShouldNotBeEmptyInteger", 1, false},
{"ShouldBeEmptyInteger8", int8(0), true},
{"ShouldNotBeEmptyInteger8", int8(1), false},
{"ShouldBeEmptyInteger16", int16(0), true},
{"ShouldNotBeEmptyInteger16", int16(1), false},
{"ShouldBeEmptyInteger32", int32(0), true},
{"ShouldNotBeEmptyInteger32", int32(1), false},
{"ShouldBeEmptyInteger64", int64(0), true},
{"ShouldNotBeEmptyInteger64", int64(1), false},
{"ShouldBeEmptyUnsignedInteger", uint(0), true},
{"ShouldNotBeEmptyUnsignedInteger", uint(1), false},
{"ShouldBeEmptyUnsignedInteger8", uint8(0), true},
{"ShouldNotBeEmptyUnsignedInteger8", uint8(1), false},
{"ShouldBeEmptyUnsignedInteger16", uint16(0), true},
{"ShouldNotBeEmptyUnsignedInteger16", uint16(1), false},
{"ShouldBeEmptyUnsignedInteger32", uint32(0), true},
{"ShouldNotBeEmptyUnsignedInteger32", uint32(1), false},
{"ShouldBeEmptyUnsignedInteger64", uint64(0), true},
{"ShouldNotBeEmptyUnsignedInteger64", uint64(1), false},
{"ShouldBeEmptyComplex64", complex64(complex(0, 0)), true},
{"ShouldNotBeEmptyComplex64", complex64(complex(100000, 7.5)), false},
{"ShouldBeEmptyComplex128", complex128(complex(0, 0)), true},
{"ShouldNotBeEmptyComplex128", complex128(complex(100000, 7.5)), false},
{"ShouldBeEmptyFloat32", float32(0), true},
{"ShouldNotBeEmptyFloat32", float32(1), false},
{"ShouldBeEmptyFloat64", float64(0), true},
{"ShouldNotBeEmptyFloat64", float64(1), false},
{"ShouldBeEmptyBoolean", false, true},
{"ShouldNotBeEmptyBoolean", true, false},
{"ShouldNotBeEmptyStruct", struct{}{}, false},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FuncEmpty(tc.value))
})
}
}

View File

@ -1,7 +1,9 @@
package templates package templates
import ( import (
"embed"
"fmt" "fmt"
"text/template"
) )
// New creates a new templates' provider. // New creates a new templates' provider.
@ -23,6 +25,64 @@ type Provider struct {
templates Templates templates Templates
} }
// LoadTemplatedAssets takes an embed.FS and loads each templated asset document into a Template.
func (p *Provider) LoadTemplatedAssets(fs embed.FS) (err error) {
var (
data []byte
)
if data, err = fs.ReadFile("public_html/index.html"); err != nil {
return err
}
if p.templates.asset.index, err = template.
New("assets/public_html/index.html").
Funcs(FuncMap()).
Parse(string(data)); err != nil {
return err
}
if data, err = fs.ReadFile("public_html/api/index.html"); err != nil {
return err
}
if p.templates.asset.api.index, err = template.
New("assets/public_html/api/index.html").
Funcs(FuncMap()).
Parse(string(data)); err != nil {
return err
}
if data, err = fs.ReadFile("public_html/api/openapi.yml"); err != nil {
return err
}
if p.templates.asset.api.spec, err = template.
New("api/public_html/openapi.yaml").
Funcs(FuncMap()).
Parse(string(data)); err != nil {
return err
}
return nil
}
// GetAssetIndexTemplate returns a Template used to generate the React index document.
func (p *Provider) GetAssetIndexTemplate() (t Template) {
return p.templates.asset.index
}
// GetAssetOpenAPIIndexTemplate returns a Template used to generate the OpenAPI index document.
func (p *Provider) GetAssetOpenAPIIndexTemplate() (t Template) {
return p.templates.asset.api.index
}
// GetAssetOpenAPISpecTemplate returns a Template used to generate the OpenAPI specification document.
func (p *Provider) GetAssetOpenAPISpecTemplate() (t Template) {
return p.templates.asset.api.spec
}
// GetEventEmailTemplate returns an EmailTemplate used for generic event notifications.
func (p *Provider) GetEventEmailTemplate() (t *EmailTemplate) { func (p *Provider) GetEventEmailTemplate() (t *EmailTemplate) {
return p.templates.notification.event return p.templates.notification.event
} }

View File

@ -9,6 +9,17 @@ import (
// Templates is the struct which holds all the *template.Template values. // Templates is the struct which holds all the *template.Template values.
type Templates struct { type Templates struct {
notification NotificationTemplates notification NotificationTemplates
asset AssetTemplates
}
type AssetTemplates struct {
index *tt.Template
api APIAssetTemplates
}
type APIAssetTemplates struct {
index *tt.Template
spec *tt.Template
} }
// NotificationTemplates are the templates for the notification system. // NotificationTemplates are the templates for the notification system.

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"path" "path"
"path/filepath" "path/filepath"
"reflect"
"strings" "strings"
tt "text/template" tt "text/template"
) )
@ -39,24 +40,17 @@ func isSecretEnvKey(key string) (isSecretEnvKey bool) {
return false return false
} }
func templateExists(path string) (exists bool) { func fileExists(path string) (exists bool) {
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil {
return false
}
if info.IsDir() { return err == nil && !info.IsDir()
return false
}
return true
} }
func readTemplate(name, ext, category, overridePath string) (tPath string, embed bool, data []byte, err error) { func readTemplate(name, ext, category, overridePath string) (tPath string, embed bool, data []byte, err error) {
if overridePath != "" { if overridePath != "" {
tPath = filepath.Join(overridePath, name+ext) tPath = filepath.Join(overridePath, name+ext)
if templateExists(tPath) { if fileExists(tPath) {
if data, err = os.ReadFile(tPath); err != nil { if data, err = os.ReadFile(tPath); err != nil {
return tPath, false, nil, fmt.Errorf("failed to read template override at path '%s': %w", tPath, err) return tPath, false, nil, fmt.Errorf("failed to read template override at path '%s': %w", tPath, err)
} }
@ -125,3 +119,55 @@ func loadEmailTemplate(name, overridePath string) (t *EmailTemplate, err error)
return t, nil return t, nil
} }
func strval(v any) string {
switch v := v.(type) {
case string:
return v
case []byte:
return string(v)
case fmt.Stringer:
return v.String()
default:
return fmt.Sprintf("%v", v)
}
}
func strslice(v any) []string {
switch v := v.(type) {
case []string:
return v
case []any:
b := make([]string, 0, len(v))
for _, s := range v {
if s != nil {
b = append(b, strval(s))
}
}
return b
default:
val := reflect.ValueOf(v)
switch val.Kind() {
case reflect.Array, reflect.Slice:
l := val.Len()
b := make([]string, 0, l)
for i := 0; i < l; i++ {
value := val.Index(i).Interface()
if value != nil {
b = append(b, strval(value))
}
}
return b
default:
if v == nil {
return []string{}
}
return []string{strval(v)}
}
}
}

View File

@ -1,9 +1,14 @@
package templates package templates
import ( import (
"io/fs"
"os"
"path/filepath"
"testing" "testing"
"text/template"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
) )
func TestIsSecretEnvKey(t *testing.T) { func TestIsSecretEnvKey(t *testing.T) {
@ -28,3 +33,72 @@ func TestIsSecretEnvKey(t *testing.T) {
}) })
} }
} }
func TestParseTemplateDirectories(t *testing.T) {
testCases := []struct {
name, path string
}{
{"Templates", "./src"},
{"OpenAPI", "../../api"},
{"Generators", "../../cmd/authelia-gen/templates"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
funcMap := FuncMap()
if tc.name == "Generators" {
funcMap["joinX"] = FuncStringJoinX
}
var (
data []byte
)
require.NoError(t, filepath.Walk(tc.path, func(path string, info fs.FileInfo, err error) error {
if info.IsDir() {
return nil
}
name := info.Name()
if tc.name == "Templates" {
name = filepath.Base(filepath.Dir(path)) + "/" + name
}
t.Run(name, func(t *testing.T) {
data, err = os.ReadFile(path)
require.NoError(t, err)
_, err = template.New(tc.name).Funcs(funcMap).Parse(string(data))
require.NoError(t, err)
})
return nil
}))
})
}
}
func TestParseMiscTemplates(t *testing.T) {
testCases := []struct {
name, path string
}{
{"ReactIndex", "../../web/index.html"},
{"ViteEnv", "../../web/.env.production"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
data, err := os.ReadFile(tc.path)
require.NoError(t, err)
_, err = template.New(tc.name).Funcs(FuncMap()).Parse(string(data))
require.NoError(t, err)
})
}
}

View File

@ -97,40 +97,6 @@ const (
timeUnixEpochAsMicrosoftNTEpoch uint64 = 116444736000000000 timeUnixEpochAsMicrosoftNTEpoch uint64 = 116444736000000000
) )
const (
// CharSetAlphabeticLower are literally just valid alphabetic lowercase printable ASCII chars.
CharSetAlphabeticLower = "abcdefghijklmnopqrstuvwxyz"
// CharSetAlphabeticUpper are literally just valid alphabetic uppercase printable ASCII chars.
CharSetAlphabeticUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
// CharSetAlphabetic are literally just valid alphabetic printable ASCII chars.
CharSetAlphabetic = CharSetAlphabeticLower + CharSetAlphabeticUpper
// CharSetNumeric are literally just valid numeric chars.
CharSetNumeric = "0123456789"
// CharSetNumericHex are literally just valid hexadecimal printable ASCII chars.
CharSetNumericHex = CharSetNumeric + "ABCDEF"
// CharSetSymbolic are literally just valid symbolic printable ASCII chars.
CharSetSymbolic = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
// CharSetSymbolicRFC3986Unreserved are RFC3986 unreserved symbol characters.
// See https://www.rfc-editor.org/rfc/rfc3986#section-2.3.
CharSetSymbolicRFC3986Unreserved = "-._~"
// CharSetAlphaNumeric are literally just valid alphanumeric printable ASCII chars.
CharSetAlphaNumeric = CharSetAlphabetic + CharSetNumeric
// CharSetASCII are literally just valid printable ASCII chars.
CharSetASCII = CharSetAlphabetic + CharSetNumeric + CharSetSymbolic
// CharSetRFC3986Unreserved are RFC3986 unreserved characters.
// See https://www.rfc-editor.org/rfc/rfc3986#section-2.3.
CharSetRFC3986Unreserved = CharSetAlphabetic + CharSetNumeric + CharSetSymbolicRFC3986Unreserved
)
var htmlEscaper = strings.NewReplacer( var htmlEscaper = strings.NewReplacer(
"&", "&amp;", "&", "&amp;",
"<", "&lt;", "<", "&lt;",

View File

@ -1,5 +1,11 @@
package utils package utils
import (
"github.com/authelia/authelia/v4/internal/random"
)
const ( const (
testStringInput = "abcdefghijkl" testStringInput = "abcdefghijkl"
) )
var r = &random.Cryptographical{}

View File

@ -573,50 +573,3 @@ loop:
return extKeyUsage return extKeyUsage
} }
// RandomString returns a random string with a given length with values from the provided characters. When crypto is set
// to false we use math/rand and when it's set to true we use crypto/rand. The crypto option should always be set to true
// excluding when the task is time sensitive and would not benefit from extra randomness.
func RandomString(n int, characters string) (randomString string) {
return string(RandomBytes(n, characters))
}
// RandomBytes returns a random []byte with a given length with values from the provided characters. When crypto is set
// to false we use math/rand and when it's set to true we use crypto/rand. The crypto option should always be set to true
// excluding when the task is time sensitive and would not benefit from extra randomness.
func RandomBytes(n int, characters string) (bytes []byte) {
bytes = make([]byte, n)
_, _ = rand.Read(bytes)
for i, b := range bytes {
bytes[i] = characters[b%byte(len(characters))]
}
return bytes
}
func RandomInt(n int) (int, error) {
if n <= 0 {
return 0, fmt.Errorf("n must be more than 0")
}
max := big.NewInt(int64(n))
if !max.IsUint64() {
return 0, fmt.Errorf("generated max is negative")
}
value, err := rand.Int(rand.Reader, max)
if err != nil {
return 0, err
}
output := int(value.Int64())
if output < 0 {
return 0, fmt.Errorf("generated number is too big for int")
}
return output, nil
}

View File

@ -7,6 +7,8 @@ import (
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/authelia/authelia/v4/internal/random"
) )
func TestShouldHashString(t *testing.T) { func TestShouldHashString(t *testing.T) {
@ -22,7 +24,7 @@ func TestShouldHashString(t *testing.T) {
assert.Equal(t, "ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e5", anotherSum) assert.Equal(t, "ae448ac86c4e8e4dec645729708ef41873ae79c6dff84eff73360989487f08e5", anotherSum)
assert.NotEqual(t, sum, anotherSum) assert.NotEqual(t, sum, anotherSum)
randomInput := RandomString(40, CharSetAlphaNumeric) randomInput := r.StringCustom(40, random.CharSetAlphaNumeric)
randomSum := HashSHA256FromString(randomInput) randomSum := HashSHA256FromString(randomInput)
assert.NotEqual(t, randomSum, sum) assert.NotEqual(t, randomSum, sum)
@ -38,7 +40,7 @@ func TestShouldHashPath(t *testing.T) {
err = os.WriteFile(filepath.Join(dir, "anotherfile"), []byte("another\n"), 0600) err = os.WriteFile(filepath.Join(dir, "anotherfile"), []byte("another\n"), 0600)
assert.NoError(t, err) assert.NoError(t, err)
err = os.WriteFile(filepath.Join(dir, "randomfile"), []byte(RandomString(40, CharSetAlphaNumeric)+"\n"), 0600) err = os.WriteFile(filepath.Join(dir, "randomfile"), []byte(r.StringCustom(40, random.CharSetAlphaNumeric)+"\n"), 0600)
assert.NoError(t, err) assert.NoError(t, err)
sum, err := HashSHA256FromPath(filepath.Join(dir, "myfile")) sum, err := HashSHA256FromPath(filepath.Join(dir, "myfile"))

View File

@ -53,13 +53,6 @@ func TestStringJoinDelimitedEscaped(t *testing.T) {
} }
} }
func TestShouldNotGenerateSameRandomString(t *testing.T) {
randomStringOne := RandomString(10, CharSetAlphaNumeric)
randomStringTwo := RandomString(10, CharSetAlphaNumeric)
assert.NotEqual(t, randomStringOne, randomStringTwo)
}
func TestShouldDetectAlphaNumericString(t *testing.T) { func TestShouldDetectAlphaNumericString(t *testing.T) {
assert.True(t, IsStringAlphaNumeric("abc")) assert.True(t, IsStringAlphaNumeric("abc"))
assert.True(t, IsStringAlphaNumeric("abc123")) assert.True(t, IsStringAlphaNumeric("abc123"))

View File

@ -26,16 +26,15 @@
"@fortawesome/free-solid-svg-icons": "6.2.1", "@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/react-fontawesome": "0.2.0", "@fortawesome/react-fontawesome": "0.2.0",
"@mui/icons-material": "5.11.0", "@mui/icons-material": "5.11.0",
"@mui/material": "5.11.2", "@mui/material": "5.11.3",
"@mui/styles": "5.11.2", "@mui/styles": "5.11.2",
"axios": "1.2.1", "axios": "1.2.2",
"broadcast-channel": "4.18.1", "broadcast-channel": "4.20.1",
"classnames": "2.3.2", "classnames": "2.3.2",
"i18next": "22.4.6", "i18next": "22.4.8",
"i18next-browser-languagedetector": "7.0.1", "i18next-browser-languagedetector": "7.0.1",
"i18next-http-backend": "2.1.1", "i18next-http-backend": "2.1.1",
"qrcode.react": "3.1.0", "qrcode.react": "3.1.0",
"query-string": "7.1.3",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",
"react-i18next": "12.1.1", "react-i18next": "12.1.1",
@ -143,24 +142,24 @@
] ]
}, },
"devDependencies": { "devDependencies": {
"@commitlint/cli": "17.3.0", "@commitlint/cli": "17.4.0",
"@commitlint/config-conventional": "17.3.0", "@commitlint/config-conventional": "17.4.0",
"@limegrass/eslint-plugin-import-alias": "1.0.6", "@limegrass/eslint-plugin-import-alias": "1.0.6",
"@testing-library/jest-dom": "5.16.5", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "13.4.0", "@testing-library/react": "13.4.0",
"@types/jest": "29.2.4", "@types/jest": "29.2.5",
"@types/node": "18.11.18", "@types/node": "18.11.18",
"@types/qrcode.react": "1.0.2", "@types/qrcode.react": "1.0.2",
"@types/react": "18.0.26", "@types/react": "18.0.26",
"@types/react-dom": "18.0.10", "@types/react-dom": "18.0.10",
"@types/zxcvbn": "4.4.1", "@types/zxcvbn": "4.4.1",
"@typescript-eslint/eslint-plugin": "5.47.1", "@typescript-eslint/eslint-plugin": "5.48.0",
"@typescript-eslint/parser": "5.47.1", "@typescript-eslint/parser": "5.48.0",
"@vitejs/plugin-react": "3.0.0", "@vitejs/plugin-react": "3.0.1",
"esbuild": "0.16.10", "esbuild": "0.16.14",
"esbuild-jest": "0.5.0", "esbuild-jest": "0.5.0",
"eslint": "8.30.0", "eslint": "8.31.0",
"eslint-config-prettier": "8.5.0", "eslint-config-prettier": "8.6.0",
"eslint-config-react-app": "7.0.1", "eslint-config-react-app": "7.0.1",
"eslint-formatter-rdjson": "1.0.5", "eslint-formatter-rdjson": "1.0.5",
"eslint-import-resolver-typescript": "3.5.2", "eslint-import-resolver-typescript": "3.5.2",
@ -169,7 +168,7 @@
"eslint-plugin-prettier": "4.2.1", "eslint-plugin-prettier": "4.2.1",
"eslint-plugin-react": "7.31.11", "eslint-plugin-react": "7.31.11",
"eslint-plugin-react-hooks": "4.6.0", "eslint-plugin-react-hooks": "4.6.0",
"husky": "8.0.2", "husky": "8.0.3",
"jest": "29.3.1", "jest": "29.3.1",
"jest-environment-jsdom": "29.3.1", "jest-environment-jsdom": "29.3.1",
"jest-transform-stub": "2.0.0", "jest-transform-stub": "2.0.0",
@ -177,7 +176,7 @@
"prettier": "2.8.1", "prettier": "2.8.1",
"react-test-renderer": "18.2.0", "react-test-renderer": "18.2.0",
"typescript": "4.9.4", "typescript": "4.9.4",
"vite": "4.0.3", "vite": "4.0.4",
"vite-plugin-eslint": "1.8.1", "vite-plugin-eslint": "1.8.1",
"vite-plugin-istanbul": "3.0.4", "vite-plugin-istanbul": "3.0.4",
"vite-plugin-svgr": "2.4.0", "vite-plugin-svgr": "2.4.0",

File diff suppressed because it is too large Load Diff

View File

@ -1 +1,7 @@
export const Identifier = "id"; export const Identifier: string = "id";
export const IdentityToken: string = "token";
export const RedirectionURL: string = "rd";
export const RequestMethod: string = "rm";

View File

@ -0,0 +1,7 @@
import { useSearchParams } from "react-router-dom";
export function useQueryParam(queryParam: string) {
const [searchParams] = useSearchParams();
const value = searchParams.get(queryParam);
return value !== "" ? (value as string) : undefined;
}

View File

@ -1,10 +0,0 @@
import queryString from "query-string";
import { useLocation } from "react-router-dom";
export function useRedirectionURL() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
return queryParams && "rd" in queryParams ? (queryParams["rd"] as string) : undefined;
}

View File

@ -1,8 +0,0 @@
import queryString from "query-string";
import { useLocation } from "react-router-dom";
export function useRequestMethod() {
const location = useLocation();
const queryParams = queryString.parse(location.search);
return queryParams && "rm" in queryParams ? (queryParams["rm"] as string) : undefined;
}

View File

@ -1,6 +0,0 @@
import queryString from "query-string";
export function extractIdentityToken(locationSearch: string) {
const queryParams = queryString.parse(locationSearch);
return queryParams && "token" in queryParams ? (queryParams["token"] as string) : null;
}

View File

@ -8,20 +8,20 @@ import makeStyles from "@mui/styles/makeStyles";
import classnames from "classnames"; import classnames from "classnames";
import { QRCodeSVG } from "qrcode.react"; import { QRCodeSVG } from "qrcode.react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import AppStoreBadges from "@components/AppStoreBadges"; import AppStoreBadges from "@components/AppStoreBadges";
import { GoogleAuthenticator } from "@constants/constants"; import { GoogleAuthenticator } from "@constants/constants";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { IdentityToken } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useQueryParam } from "@hooks/QueryParam";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { completeTOTPRegistrationProcess } from "@services/RegisterDevice"; import { completeTOTPRegistrationProcess } from "@services/RegisterDevice";
import { extractIdentityToken } from "@utils/IdentityToken";
const RegisterOneTimePassword = function () { const RegisterOneTimePassword = function () {
const styles = useStyles(); const styles = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
// The secret retrieved from the API is all is ok. // The secret retrieved from the API is all is ok.
const [secretURL, setSecretURL] = useState("empty"); const [secretURL, setSecretURL] = useState("empty");
const [secretBase32, setSecretBase32] = useState(undefined as string | undefined); const [secretBase32, setSecretBase32] = useState(undefined as string | undefined);
@ -32,7 +32,7 @@ const RegisterOneTimePassword = function () {
// Get the token from the query param to give it back to the API when requesting // Get the token from the query param to give it back to the API when requesting
// the secret for OTP. // the secret for OTP.
const processToken = extractIdentityToken(location.search); const processToken = useQueryParam(IdentityToken);
const handleDoneClick = () => { const handleDoneClick = () => {
navigate(IndexRoute); navigate(IndexRoute);

View File

@ -10,7 +10,9 @@ import InformationIcon from "@components/InformationIcon";
import SuccessIcon from "@components/SuccessIcon"; import SuccessIcon from "@components/SuccessIcon";
import WebauthnTryIcon from "@components/WebauthnTryIcon"; import WebauthnTryIcon from "@components/WebauthnTryIcon";
import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes"; import { SettingsRoute, SettingsTwoFactorAuthenticationSubRoute } from "@constants/Routes";
import { IdentityToken } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useQueryParam } from "@hooks/QueryParam";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { AttestationPublicKeyCredential, AttestationResult, WebauthnTouchState } from "@models/Webauthn"; import { AttestationPublicKeyCredential, AttestationResult, WebauthnTouchState } from "@models/Webauthn";
import { import {
@ -18,7 +20,6 @@ import {
getAttestationCreationOptions, getAttestationCreationOptions,
getAttestationPublicKeyCredentialResult, getAttestationPublicKeyCredentialResult,
} from "@services/Webauthn"; } from "@services/Webauthn";
import { extractIdentityToken } from "@utils/IdentityToken";
const steps = ["Confirm device", "Choose name"]; const steps = ["Confirm device", "Choose name"];
@ -39,7 +40,7 @@ const RegisterWebauthn = function (props: Props) {
const nameRef = useRef() as MutableRefObject<HTMLInputElement>; const nameRef = useRef() as MutableRefObject<HTMLInputElement>;
const [nameError, setNameError] = useState(false); const [nameError, setNameError] = useState(false);
const processToken = extractIdentityToken(location.search); const processToken = useQueryParam(IdentityToken);
const handleBackClick = () => { const handleBackClick = () => {
navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`); navigate(`${SettingsRoute}${SettingsTwoFactorAuthenticationSubRoute}`);

View File

@ -9,9 +9,9 @@ import { useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField"; import FixedTextField from "@components/FixedTextField";
import { ResetPasswordStep1Route } from "@constants/Routes"; import { ResetPasswordStep1Route } from "@constants/Routes";
import { RedirectionURL, RequestMethod } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useQueryParam } from "@hooks/QueryParam";
import { useRequestMethod } from "@hooks/RequestMethod";
import { useWorkflow } from "@hooks/Workflow"; import { useWorkflow } from "@hooks/Workflow";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { postFirstFactor } from "@services/FirstFactor"; import { postFirstFactor } from "@services/FirstFactor";
@ -32,8 +32,8 @@ export interface Props {
const FirstFactorForm = function (props: Props) { const FirstFactorForm = function (props: Props) {
const styles = useStyles(); const styles = useStyles();
const navigate = useNavigate(); const navigate = useNavigate();
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const requestMethod = useRequestMethod(); const requestMethod = useQueryParam(RequestMethod);
const [workflow] = useWorkflow(); const [workflow] = useWorkflow();
const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []); const loginChannel = useMemo(() => new BroadcastChannel<boolean>("login"), []);

View File

@ -10,9 +10,10 @@ import {
SecondFactorTOTPSubRoute, SecondFactorTOTPSubRoute,
SecondFactorWebauthnSubRoute, SecondFactorWebauthnSubRoute,
} from "@constants/Routes"; } from "@constants/Routes";
import { RedirectionURL } from "@constants/SearchParams";
import { useConfiguration } from "@hooks/Configuration"; import { useConfiguration } from "@hooks/Configuration";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useQueryParam } from "@hooks/QueryParam";
import { useRedirector } from "@hooks/Redirector"; import { useRedirector } from "@hooks/Redirector";
import { useRouterNavigate } from "@hooks/RouterNavigate"; import { useRouterNavigate } from "@hooks/RouterNavigate";
import { useAutheliaState } from "@hooks/State"; import { useAutheliaState } from "@hooks/State";
@ -38,7 +39,7 @@ const RedirectionErrorMessage =
const LoginPortal = function (props: Props) { const LoginPortal = function (props: Props) {
const location = useLocation(); const location = useLocation();
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
const [firstFactorDisabled, setFirstFactorDisabled] = useState(true); const [firstFactorDisabled, setFirstFactorDisabled] = useState(true);
const [broadcastRedirect, setBroadcastRedirect] = useState(false); const [broadcastRedirect, setBroadcastRedirect] = useState(false);

View File

@ -34,6 +34,7 @@ const OTPDial = function (props: Props) {
isDisabled={props.state === State.InProgress || props.state === State.Success} isDisabled={props.state === State.InProgress || props.state === State.Success}
isInputNum isInputNum
hasErrored={props.state === State.Failure} hasErrored={props.state === State.Failure}
autoComplete="one-time-code"
inputStyle={classnames( inputStyle={classnames(
styles.otpDigitInput, styles.otpDigitInput,
props.state === State.Failure ? styles.inputError : "", props.state === State.Failure ? styles.inputError : "",

View File

@ -2,7 +2,8 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { RedirectionURL } from "@constants/SearchParams";
import { useQueryParam } from "@hooks/QueryParam";
import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration"; import { useUserInfoTOTPConfiguration } from "@hooks/UserInfoTOTPConfiguration";
import { useWorkflow } from "@hooks/Workflow"; import { useWorkflow } from "@hooks/Workflow";
import { completeTOTPSignIn } from "@services/OneTimePassword"; import { completeTOTPSignIn } from "@services/OneTimePassword";
@ -33,7 +34,7 @@ const OneTimePasswordMethod = function (props: Props) {
const [state, setState] = useState( const [state, setState] = useState(
props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle, props.authenticationLevel === AuthenticationLevel.TwoFactor ? State.Success : State.Idle,
); );
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const [workflow, workflowID] = useWorkflow(); const [workflow, workflowID] = useWorkflow();
const { t: translate } = useTranslation(); const { t: translate } = useTranslation();

View File

@ -6,8 +6,9 @@ import makeStyles from "@mui/styles/makeStyles";
import FailureIcon from "@components/FailureIcon"; import FailureIcon from "@components/FailureIcon";
import PushNotificationIcon from "@components/PushNotificationIcon"; import PushNotificationIcon from "@components/PushNotificationIcon";
import SuccessIcon from "@components/SuccessIcon"; import SuccessIcon from "@components/SuccessIcon";
import { RedirectionURL } from "@constants/SearchParams";
import { useIsMountedRef } from "@hooks/Mounted"; import { useIsMountedRef } from "@hooks/Mounted";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useQueryParam } from "@hooks/QueryParam";
import { useWorkflow } from "@hooks/Workflow"; import { useWorkflow } from "@hooks/Workflow";
import { import {
DuoDevicePostRequest, DuoDevicePostRequest,
@ -44,7 +45,7 @@ export interface Props {
const PushNotificationMethod = function (props: Props) { const PushNotificationMethod = function (props: Props) {
const styles = useStyles(); const styles = useStyles();
const [state, setState] = useState(State.SignInInProgress); const [state, setState] = useState(State.SignInInProgress);
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const [workflow, workflowID] = useWorkflow(); const [workflow, workflowID] = useWorkflow();
const mounted = useIsMountedRef(); const mounted = useIsMountedRef();
const [enroll_url, setEnrollUrl] = useState(""); const [enroll_url, setEnrollUrl] = useState("");

View File

@ -1,8 +1,9 @@
import React, { useCallback, useEffect, useRef, useState } from "react"; import React, { useCallback, useEffect, useRef, useState } from "react";
import WebauthnTryIcon from "@components/WebauthnTryIcon"; import WebauthnTryIcon from "@components/WebauthnTryIcon";
import { RedirectionURL } from "@constants/SearchParams";
import { useIsMountedRef } from "@hooks/Mounted"; import { useIsMountedRef } from "@hooks/Mounted";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useQueryParam } from "@hooks/QueryParam";
import { useWorkflow } from "@hooks/Workflow"; import { useWorkflow } from "@hooks/Workflow";
import { AssertionResult, WebauthnTouchState } from "@models/Webauthn"; import { AssertionResult, WebauthnTouchState } from "@models/Webauthn";
import { AuthenticationLevel } from "@services/State"; import { AuthenticationLevel } from "@services/State";
@ -25,7 +26,7 @@ export interface Props {
const WebauthnMethod = function (props: Props) { const WebauthnMethod = function (props: Props) {
const [state, setState] = useState(WebauthnTouchState.WaitTouch); const [state, setState] = useState(WebauthnTouchState.WaitTouch);
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const [workflow, workflowID] = useWorkflow(); const [workflow, workflowID] = useWorkflow();
const mounted = useIsMountedRef(); const mounted = useIsMountedRef();

View File

@ -6,9 +6,10 @@ import { useTranslation } from "react-i18next";
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { RedirectionURL } from "@constants/SearchParams";
import { useIsMountedRef } from "@hooks/Mounted"; import { useIsMountedRef } from "@hooks/Mounted";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useRedirectionURL } from "@hooks/RedirectionURL"; import { useQueryParam } from "@hooks/QueryParam";
import { useRedirector } from "@hooks/Redirector"; import { useRedirector } from "@hooks/Redirector";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { signOut } from "@services/SignOut"; import { signOut } from "@services/SignOut";
@ -19,7 +20,7 @@ const SignOut = function (props: Props) {
const mounted = useIsMountedRef(); const mounted = useIsMountedRef();
const styles = useStyles(); const styles = useStyles();
const { createErrorNotification } = useNotifications(); const { createErrorNotification } = useNotifications();
const redirectionURL = useRedirectionURL(); const redirectionURL = useQueryParam(RedirectionURL);
const redirector = useRedirector(); const redirector = useRedirector();
const [timedOut, setTimedOut] = useState(false); const [timedOut, setTimedOut] = useState(false);
const [safeRedirect, setSafeRedirect] = useState(false); const [safeRedirect, setSafeRedirect] = useState(false);

View File

@ -5,21 +5,21 @@ import { Button, Grid, IconButton, InputAdornment, Theme } from "@mui/material";
import makeStyles from "@mui/styles/makeStyles"; import makeStyles from "@mui/styles/makeStyles";
import classnames from "classnames"; import classnames from "classnames";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useLocation, useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import FixedTextField from "@components/FixedTextField"; import FixedTextField from "@components/FixedTextField";
import PasswordMeter from "@components/PasswordMeter"; import PasswordMeter from "@components/PasswordMeter";
import { IndexRoute } from "@constants/Routes"; import { IndexRoute } from "@constants/Routes";
import { IdentityToken } from "@constants/SearchParams";
import { useNotifications } from "@hooks/NotificationsContext"; import { useNotifications } from "@hooks/NotificationsContext";
import { useQueryParam } from "@hooks/QueryParam";
import LoginLayout from "@layouts/LoginLayout"; import LoginLayout from "@layouts/LoginLayout";
import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy"; import { PasswordPolicyConfiguration, PasswordPolicyMode } from "@models/PasswordPolicy";
import { getPasswordPolicyConfiguration } from "@services/PasswordPolicyConfiguration"; import { getPasswordPolicyConfiguration } from "@services/PasswordPolicyConfiguration";
import { completeResetPasswordProcess, resetPassword } from "@services/ResetPassword"; import { completeResetPasswordProcess, resetPassword } from "@services/ResetPassword";
import { extractIdentityToken } from "@utils/IdentityToken";
const ResetPasswordStep2 = function () { const ResetPasswordStep2 = function () {
const styles = useStyles(); const styles = useStyles();
const location = useLocation();
const [formDisabled, setFormDisabled] = useState(true); const [formDisabled, setFormDisabled] = useState(true);
const [password1, setPassword1] = useState(""); const [password1, setPassword1] = useState("");
const [password2, setPassword2] = useState(""); const [password2, setPassword2] = useState("");
@ -43,7 +43,7 @@ const ResetPasswordStep2 = function () {
// Get the token from the query param to give it back to the API when requesting // Get the token from the query param to give it back to the API when requesting
// the secret for OTP. // the secret for OTP.
const processToken = extractIdentityToken(location.search); const processToken = useQueryParam(IdentityToken);
const completeProcess = useCallback(async () => { const completeProcess = useCallback(async () => {
if (!processToken) { if (!processToken) {