From 08ad5a510d6c45c36aa13e389dfe528622604b30 Mon Sep 17 00:00:00 2001 From: Spoffy Date: Fri, 26 Jul 2024 15:57:46 +0100 Subject: [PATCH] Working OIDC with Authelia --- .../grist-local-testing/README.md | 0 .../grist-traefik-oidc-auth/README.md | 15 ++ .../configs/authelia/configuration.yml | 47 +++-- .../configs/dex/config.yaml | 61 ------- .../configs/traefik-dynamic-config.yml | 2 - .../config.yml} | 7 +- .../docker-compose.yml | 164 +++++++++--------- .../generateSecureSecrets.sh | 32 ++-- .../secrets/GRIST_CLIENT_SECRET_DIGEST | 0 9 files changed, 139 insertions(+), 189 deletions(-) create mode 100644 docker-compose-examples/grist-local-testing/README.md create mode 100644 docker-compose-examples/grist-traefik-oidc-auth/README.md delete mode 100644 docker-compose-examples/grist-traefik-oidc-auth/configs/dex/config.yaml delete mode 100644 docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-dynamic-config.yml rename docker-compose-examples/grist-traefik-oidc-auth/configs/{traefik-config.yml => traefik/config.yml} (74%) create mode 100644 docker-compose-examples/grist-traefik-oidc-auth/secrets/GRIST_CLIENT_SECRET_DIGEST diff --git a/docker-compose-examples/grist-local-testing/README.md b/docker-compose-examples/grist-local-testing/README.md new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose-examples/grist-traefik-oidc-auth/README.md b/docker-compose-examples/grist-traefik-oidc-auth/README.md new file mode 100644 index 00000000..f30987ea --- /dev/null +++ b/docker-compose-examples/grist-traefik-oidc-auth/README.md @@ -0,0 +1,15 @@ +This is an example of Grist with Authelia for OIDC authentication, and Traefik for HTTP encryption and routing. + +OIDC enables authentication using many existing providers, including Google, Microsoft, Amazon and Okta. + +This example uses Authelia, which is a locally hosted OIDC provider, so that it can work without further setup. +However, Authelia could be easily replaced by one of the providers listed above, or other self-hosted alternatives, +such as Authentik or Dex. + +This example could be hosted on a dedicated server, with the following changes: +- DNS setup +- HTTPS / Certificate setup (e.g Let's encrypt) + +Users are defined in ./configs/authelia/user-database.yml + +See https://support.getgrist.com/install/oidc for more information on using Grist with OIDC. diff --git a/docker-compose-examples/grist-traefik-oidc-auth/configs/authelia/configuration.yml b/docker-compose-examples/grist-traefik-oidc-auth/configs/authelia/configuration.yml index 1c57c7cd..dc0c720c 100644 --- a/docker-compose-examples/grist-traefik-oidc-auth/configs/authelia/configuration.yml +++ b/docker-compose-examples/grist-traefik-oidc-auth/configs/authelia/configuration.yml @@ -39,7 +39,7 @@ server: ## Square brackets indicate optional portions of the format. Scheme must be 'tcp', 'tcp4', 'tcp6', or 'unix'. ## The default scheme is 'unix' if the address is an absolute path otherwise it's 'tcp'. The default port is '9091'. ## If the path is specified this configures the router to handle both the `/` path and the configured path. - address: 'tcp://:9091/authelia' + address: 'tcp://:9091/' ## Set the path on disk to Authelia assets. ## Useful to allow overriding of specific static assets. @@ -1137,37 +1137,34 @@ notifier: ## ## Identity Providers ## -# identity_providers: +identity_providers: ## ## OpenID Connect (Identity Provider) ## ## It's recommended you read the documentation before configuration of this section: ## https://www.authelia.com/c/oidc - # oidc: + oidc: ## The hmac_secret is used to sign OAuth2 tokens (authorization code, access tokens and refresh tokens). ## HMAC Secret can also be set using a secret: https://www.authelia.com/c/secrets - # hmac_secret: 'this_is_a_secret_abc123abc123abc' + hmac_secret: {{ secret (mustEnv "HMAC_SECRET_FILE") }} ## The JWK's issuer option configures multiple JSON Web Keys. It's required that at least one of the JWK's ## configured has the RS256 algorithm. For RSA keys (RS or PS) the minimum is a 2048 bit key. - # jwks: - # - + jwks: + - ## Key ID embedded into the JWT header for key matching. Must be an alphanumeric string with 7 or less characters. ## This value is automatically generated if not provided. It's recommended to not configure this. # key_id: 'example' ## The key algorithm used with this key. - # algorithm: 'RS256' + algorithm: 'RS256' ## The key use expected with this key. Currently only 'sig' is supported. - # use: 'sig' + use: 'sig' ## Required Private Key in PEM DER form. - # key: | - # -----BEGIN RSA PRIVATE KEY----- - # ... - # -----END RSA PRIVATE KEY----- + key: {{ secret (mustEnv "JWT_PRIVATE_KEY_FILE") | mindent 10 "|" | msquote }} ## Optional matching certificate chain in PEM DER form that matches the key. All certificates within the chain @@ -1243,18 +1240,18 @@ notifier: # allowed_origins_from_client_redirect_uris: false ## Clients is a list of known clients and their configuration. - # clients: - # - + clients: + - ## The Client ID is the OAuth 2.0 and OpenID Connect 1.0 Client ID which is used to link an application to a ## configuration. - # client_id: 'myapp' + client_id: 'grist-local' ## The description to show to users when they end up on the consent screen. Defaults to the ID above. - # client_name: 'My Application' + client_name: 'Grist' ## The client secret is a shared secret between Authelia and the consumer of this client. # yamllint disable-line rule:line-length - # client_secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'. + client_secret: {{ secret (mustEnv "GRIST_CLIENT_SECRET_DIGEST_FILE") }} ## Sector Identifiers are occasionally used to generate pairwise subject identifiers. In most cases this is not ## necessary. It is critical to read the documentation for more information. @@ -1264,8 +1261,8 @@ notifier: # public: false ## Redirect URI's specifies a list of valid case-sensitive callbacks for this client. - # redirect_uris: - # - 'https://oidc.example.com:8080/oauth2/callback' + redirect_uris: + - {{ mustEnv "GRIST_OAUTH_CALLBACK_URL" | quote }} ## Request URI's specifies a list of valid case-sensitive TLS-secured URIs for this client for use as ## URIs to fetch Request Objects. @@ -1276,11 +1273,11 @@ notifier: # audience: [] ## Scopes this client is allowed to request. - # scopes: - # - 'openid' - # - 'groups' - # - 'email' - # - 'profile' + scopes: + - 'openid' + - 'groups' + - 'email' + - 'profile' ## Grant Types configures which grants this client can obtain. ## It's not recommended to define this unless you know what you're doing. @@ -1299,7 +1296,7 @@ notifier: ## The policy to require for this client; one_factor or two_factor. Can also be the key names for the ## authorization policies section. - # authorization_policy: 'two_factor' + authorization_policy: 'one_factor' ## The custom lifespan name to use for this client. This must be configured independent of the client before ## utilization. Custom lifespans are reusable similar to authorization policies. diff --git a/docker-compose-examples/grist-traefik-oidc-auth/configs/dex/config.yaml b/docker-compose-examples/grist-traefik-oidc-auth/configs/dex/config.yaml deleted file mode 100644 index 75fc136b..00000000 --- a/docker-compose-examples/grist-traefik-oidc-auth/configs/dex/config.yaml +++ /dev/null @@ -1,61 +0,0 @@ -# This file uses Go template formatting. - -issuer: {{ getenv "DEX_ISSUER" "http://127.0.0.1:5556/dex" }} - -storage: - type: sqlite3 - config: - file: {{ getenv "DEX_STORAGE_SQLITE3_CONFIG_FILE" "/var/dex/dex.db" }} - -web: -{{- if getenv "DEX_WEB_HTTPS" "" }} - https: {{ .Env.DEX_WEB_HTTPS }} - tlsKey: {{ getenv "DEX_WEB_TLS_KEY" | required "$DEX_WEB_TLS_KEY in case of web.https is enabled" }} - tlsCert: {{ getenv "DEX_WEB_TLS_CERT" | required "$DEX_WEB_TLS_CERT in case of web.https is enabled" }} -{{- end }} - http: {{ getenv "DEX_WEB_HTTP" "0.0.0.0:5556" }} - -{{- if getenv "DEX_TELEMETRY_HTTP" }} -telemetry: - http: {{ .Env.DEX_TELEMETRY_HTTP }} -{{- end }} - -expiry: - deviceRequests: {{ getenv "DEX_EXPIRY_DEVICE_REQUESTS" "5m" }} - signingKeys: {{ getenv "DEX_EXPIRY_SIGNING_KEYS" "6h" }} - idTokens: {{ getenv "DEX_EXPIRY_ID_TOKENS" "24h" }} - authRequests: {{ getenv "DEX_EXPIRY_AUTH_REQUESTS" "24h" }} - -logger: - level: {{ getenv "DEX_LOG_LEVEL" "info" }} - format: {{ getenv "DEX_LOG_FORMAT" "text" }} - -oauth2: - responseTypes: {{ getenv "DEX_OAUTH2_RESPONSE_TYPES" "[code]" }} - skipApprovalScreen: {{ getenv "DEX_OAUTH2_SKIP_APPROVAL_SCREEN" "false" }} - alwaysShowLoginScreen: {{ getenv "DEX_OAUTH2_ALWAYS_SHOW_LOGIN_SCREEN" "false" }} -{{- if getenv "DEX_OAUTH2_PASSWORD_CONNECTOR" "" }} - passwordConnector: {{ .Env.DEX_OAUTH2_PASSWORD_CONNECTOR }} -{{- end }} - -enablePasswordDB: {{ getenv "DEX_ENABLE_PASSWORD_DB" "true" }} - -staticPasswords: - - email: "admin@example.com" - hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W" - username: "admin" - userID: "08a8684b-db88-4b73-90a9-3cd1661f5466" - -staticClients: - - id: grist-client - secret: app-secret - name: 'Private Client' - redirectURIs: - - 'https://example.com/oidc/callback' - -connectors: -{{- if getenv "DEX_CONNECTORS_ENABLE_MOCK" }} -- type: mockCallback - id: mock - name: Example -{{- end }} \ No newline at end of file diff --git a/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-dynamic-config.yml b/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-dynamic-config.yml deleted file mode 100644 index 87861bc8..00000000 --- a/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-dynamic-config.yml +++ /dev/null @@ -1,2 +0,0 @@ -http: - # Declaring the user list \ No newline at end of file diff --git a/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-config.yml b/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik/config.yml similarity index 74% rename from docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-config.yml rename to docker-compose-examples/grist-traefik-oidc-auth/configs/traefik/config.yml index 27741801..2469bd32 100644 --- a/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik-config.yml +++ b/docker-compose-examples/grist-traefik-oidc-auth/configs/traefik/config.yml @@ -1,9 +1,6 @@ providers: # Enables reading docker label config values docker: {} - # Read additional config from this file. - file: - directory: "/etc/traefik/dynamic" entrypoints: # Defines a secure entrypoint using TLS encryption @@ -29,7 +26,5 @@ certificatesResolvers: storage: /acme/acme.json tlschallenge: true -# Enables the web UI -# This is disabled by default for security, but can be useful to debugging traefik. api: - # insecure: true \ No newline at end of file + insecure: true \ No newline at end of file diff --git a/docker-compose-examples/grist-traefik-oidc-auth/docker-compose.yml b/docker-compose-examples/grist-traefik-oidc-auth/docker-compose.yml index 49863df2..32926d60 100644 --- a/docker-compose-examples/grist-traefik-oidc-auth/docker-compose.yml +++ b/docker-compose-examples/grist-traefik-oidc-auth/docker-compose.yml @@ -1,13 +1,3 @@ -# This is an example of Grist using Authelia and Traefik for OIDC authentication and https encryption. - -# At a minimum, the following should be changed before hosting this example on the internet: -# - An SMTP notifier should be setup to allow Authelia to send emails, instead of logging to a file. -# - DNS should be setup appropriately - -# Users are defined in ./configs/authelia/user-database.yml - -# See https://support.getgrist.com for more information. - secrets: # These secrets are used by Authelia JWT_SECRET: @@ -19,17 +9,27 @@ secrets: # These secrets are for using Authelia as an OIDC provider HMAC_SECRET: file: ./secrets/HMAC_SECRET + JWT_PRIVATE_KEY: + file: ./secrets/certs/private.pem + GRIST_CLIENT_SECRET_DIGEST: + file: ./secrets/GRIST_CLIENT_SECRET_DIGEST services: grist: image: gristlabs/grist:latest - ports: - - 8484:8484 environment: - GRIST_OIDC_IDP_ISSUER: http://dex:5556 - GRIST_OIDC_IDP_CLIENT_ID: grist-client - GRIST_OIDC_IDP_CLIENT_SECRET: app-secret - GRIST_OIDC_IDP_SKIP_END_SESSION_ENDPOINT: true + # The URL of given OIDC provider. Used for redirects, among other things. + GRIST_OIDC_IDP_ISSUER: https://auth.grist.localhost + # Client ID, as configured with the OIDC provider. + GRIST_OIDC_IDP_CLIENT_ID: grist-local + # Client secret, as provided by the OIDC provider. + GRIST_OIDC_IDP_CLIENT_SECRET: ${GRIST_CLIENT_SECRET} + # The URL to redirect to with the OIDC provider to log out. + # Some OIDC providers will automatically configure this. + GRIST_OIDC_IDP_END_SESSION_ENDPOINT: https://auth.grist.localhost/logout + # Allow self-signed certificates so this example behaves correctly. + # REMOVE THIS IF HOSTING ON THE INTERNET. + NODE_TLS_REJECT_UNAUTHORIZED: 0 # Forces Grist to only use a single team called 'Example' GRIST_SINGLE_ORG: my-grist-team # alternatively, GRIST_ORG_IN_PATH: "true" for multi-team operation @@ -39,6 +39,7 @@ services: APP_HOME_URL: https://grist.localhost # Default email for the "Admin" account GRIST_DEFAULT_EMAIL: test@example.org + restart: always volumes: # Where to store persistent data, such as documents. - ./grist_local_data:/persist @@ -46,74 +47,71 @@ services: - "traefik.http.services.grist.loadbalancer.server.port=8484" - "traefik.http.routers.grist.rule=Host(`grist.localhost`)" - "traefik.http.routers.grist.service=grist" - - "traefik.http.routers.grist.tls.certresolver=letsencrypt" -# -# traefik: -# image: traefik:latest -# ports: -# # HTTP Ports -# - "80:80" -# - "443:443" -# # The Web UI (enabled by --api.insecure=true) -# # - "8080:8080" -# volumes: -# # Set the config file for traefik - this is loaded automatically. -# - ./configs/traefik-config.yml:/etc/traefik/traefik.yml -# # Set the config file for the dynamic config, such as middleware. -# - ./configs/traefik-dynamic-config.yml:/etc/traefik/dynamic/dynamic-config.yml -# # You may want to put state somewhere other than /tmp :-) -# - /tmp/grist/acme:/acme -# # Traefik needs docker access when configured via docker labels. -# - /var/run/docker.sock:/var/run/docker.sock -# depends_on: -# grist: -# condition: service_started -# authelia: -# condition: service_started + # Uncomment and configure in traefik-config.yml to enable automatic HTTPS certificate setup. + #- "traefik.http.routers.grist.tls.certresolver=letsencrypt" + depends_on: + # Grist attempts to setup OIDC when it starts, making a request to the OIDC service. + # This will fail if Authelia isn't ready and reachable. + # Traefik will only start routing to Authelia when it's registered as healthy. + # Making Grist wait for Authelia to be healthy should avoid this issue. + authelia: + condition: service_healthy + traefik: + condition: service_started -# authelia: -# image: authelia/authelia:4 -# ports: -# - 9091:9091 -# secrets: -# - HMAC_SECRET -# - JWT_SECRET -# - SESSION_SECRET -# - STORAGE_ENCRYPTION_KEY -# environment: -# AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET' -# AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET' -# AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY' -# # Domain Grist is hosted at. Custom variable that's interpolated into the Authelia config -# APP_DOMAIN: 'grist.localhost' -# volumes: -# - ./configs/authelia:/config -# command: -# - 'authelia' -# - '--config=/config/configuration.yml' -# # Enables passing environment variables down to the Authelia config. -# - '--config.experimental.filters=template' -# labels: -# - "traefik.http.services.authelia.loadbalancer.server.port=9091" -# - "traefik.http.routers.authelia.rule=Host(`auth.grist.localhost`)" -# - "traefik.http.routers.authelia.service=authelia" -# - "traefik.http.routers.authelia.tls.certresolver=letsencrypt" - - dex: - image: dexidp/dex:latest + traefik: + image: traefik:latest ports: - - 5556:5556 - - 5557:5557 - environment: - DEX_ISSUER: http://auth.grist.localhost:5556/ - DEX_STORAGE_SQLITE3_CONFIG_FILE: /dex_db/dex.db - DEX_ENABLE_PASSWORD_DB: true - DEX_OAUTH2_PASSWORD_CONNECTOR: local + # HTTP Ports + - "80:80" + - "443:443" + # The Web UI (enabled by --api.insecure=true) + - "8080:8080" + - "8082:8082" volumes: - - ./configs/dex:/config - - ./dex_db:/dex_db - command: - - dex - - serve - - /config/config.yaml + # Set the config file for traefik - this is loaded automatically. + - ./configs/traefik/config.yml:/etc/traefik/traefik.yml + # Certificate location, if automatic certificate setup is enabled. + - ./configs/traefik/acme:/acme + # Traefik needs docker access when configured via docker labels. + - /var/run/docker.sock:/var/run/docker.sock + networks: + default: + aliases: + # Enables Grist to resolve this domain to Traefik when doing OIDC setup. + - auth.grist.localhost + authelia: + image: authelia/authelia:4 + secrets: + - HMAC_SECRET + - JWT_SECRET + - JWT_PRIVATE_KEY + - GRIST_CLIENT_SECRET_DIGEST + - SESSION_SECRET + - STORAGE_ENCRYPTION_KEY + environment: + AUTHELIA_IDENTITY_VALIDATION_RESET_PASSWORD_JWT_SECRET_FILE: '/run/secrets/JWT_SECRET' + AUTHELIA_SESSION_SECRET_FILE: '/run/secrets/SESSION_SECRET' + AUTHELIA_STORAGE_ENCRYPTION_KEY_FILE: '/run/secrets/STORAGE_ENCRYPTION_KEY' + HMAC_SECRET_FILE: '/run/secrets/HMAC_SECRET' + JWT_PRIVATE_KEY_FILE: '/run/secrets/JWT_PRIVATE_KEY' + # Domain Grist is hosted at. Custom variable that's interpolated into the Authelia config + APP_DOMAIN: 'grist.localhost' + # Where Authelia should redirect to after successful authentication. + GRIST_OAUTH_CALLBACK_URL: https://grist.localhost/oauth2/callback + # Hash of the client secret provided to Grist. + GRIST_CLIENT_SECRET_DIGEST_FILE: "/run/secrets/GRIST_CLIENT_SECRET_DIGEST" + volumes: + - ./configs/authelia:/config + command: + - 'authelia' + - '--config=/config/configuration.yml' + # Enables templating in the config file + - '--config.experimental.filters=template' + labels: + - "traefik.http.services.authelia.loadbalancer.server.port=9091" + - "traefik.http.routers.authelia.rule=Host(`auth.grist.localhost`)" + - "traefik.http.routers.authelia.service=authelia" + # Uncomment and configure in traefik-config.yml to enable automatic HTTPS certificate setup. + #- "traefik.http.routers.authelia.tls.certresolver=letsencrypt" diff --git a/docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh b/docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh index 8a6090ac..9136d83c 100755 --- a/docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh +++ b/docker-compose-examples/grist-traefik-oidc-auth/generateSecureSecrets.sh @@ -1,18 +1,26 @@ # Helper script to securely generate random secrets for Authelia. -# If this doesn't work on your platform, here are some alternate snippets for secure string generation: -# Python: -# python -c "import secrets; print(secrets.token_urlsafe(32))" -# Javascript / Node: -# node -e "console.log(crypto.randomBytes(32).toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, ''))" - SCRIPT_DIR=$(dirname $0) -function generateSecureString { - xxd -l"$1" -ps /dev/urandom | xxd -r -ps | base64 \ - | tr -d = | tr + - | tr / _ | tr -d \\n +function getSecret { + cut -d ":" -f 2 <<< "$1" | tr -d '[:blank:]' } -generateSecureString 64 > "$SCRIPT_DIR/secrets/JWT_SECRET" -generateSecureString 64 > "$SCRIPT_DIR/secrets/SESSION_SECRET" -generateSecureString 64 > "$SCRIPT_DIR/secrets/STORAGE_ENCRYPTION_KEY" +function generateSecureString { + getSecret "$(docker run authelia/authelia:4 authelia crypto rand --charset=rfc3986 --length="$1")" +} + +generateSecureString 128 > "$SCRIPT_DIR/secrets/HMAC_SECRET" +generateSecureString 128 > "$SCRIPT_DIR/secrets/JWT_SECRET" +generateSecureString 128 > "$SCRIPT_DIR/secrets/SESSION_SECRET" +generateSecureString 128 > "$SCRIPT_DIR/secrets/STORAGE_ENCRYPTION_KEY" + +# Generates the OIDC secret key for the Grist client +CLIENT_SECRET_OUTPUT="$(docker run authelia/authelia:4 authelia crypto hash generate pbkdf2 --variant sha512 --random --random.length 72 --random.charset rfc3986)" +CLIENT_SECRET=$(getSecret "$(grep 'Password' <<< $CLIENT_SECRET_OUTPUT)") +echo "GRIST_CLIENT_SECRET=$CLIENT_SECRET" >> "$SCRIPT_DIR/.env" +getSecret "$(grep 'Digest' <<< $CLIENT_SECRET_OUTPUT)" >> "$SCRIPT_DIR/secrets/GRIST_CLIENT_SECRET_DIGEST" + +# Generate JWT certificates Authelia needs for OIDC +docker run -v ./secrets/certs:/certs authelia/authelia:4 authelia crypto certificate rsa generate -d /certs + diff --git a/docker-compose-examples/grist-traefik-oidc-auth/secrets/GRIST_CLIENT_SECRET_DIGEST b/docker-compose-examples/grist-traefik-oidc-auth/secrets/GRIST_CLIENT_SECRET_DIGEST new file mode 100644 index 00000000..e69de29b