From 5126d36b2ec777f67290e93dd5b998fda2358129 Mon Sep 17 00:00:00 2001 From: Quentin Gliech Date: Mon, 20 Nov 2023 14:49:57 +0100 Subject: [PATCH] Add upstream OAuth 2.0 providers name and branding --- crates/cli/src/commands/config.rs | 2 + crates/config/src/sections/upstream_oauth2.rs | 16 ++++++ .../src/upstream_oauth2/provider.rs | 2 + crates/handlers/src/upstream_oauth2/cache.rs | 2 + crates/handlers/src/upstream_oauth2/link.rs | 2 + crates/handlers/src/views/login.rs | 8 ++- ...0451f8bcf5df20faf46a4a4c0d4a36d1ff173.json | 29 ++++++++++ ...a8cdf1d21f788a533477c233455c57a1b755.json} | 38 ++++++++----- ...1376739318efa1e84670e04189e1257d4a8ed.json | 27 ---------- ...206600624bbba57985d4a7fba2763478cd065.json | 37 +++++++++++++ ...8938938f09b3215a9b14f7edb3dd91cdf2dd5.json | 35 ------------ ...c0ddb04752d374e1905d1d935313090375c9.json} | 38 ++++++++----- ...20231120110559_upstream_oauth_branding.sql | 18 +++++++ crates/storage-pg/src/iden.rs | 2 + crates/storage-pg/src/upstream_oauth2/mod.rs | 4 ++ .../src/upstream_oauth2/provider.rs | 40 +++++++++++++- .../storage/src/upstream_oauth2/provider.rs | 6 +++ docs/config.schema.json | 8 +++ templates/components/idp_brand.html | 53 +++++++++++++++++++ templates/pages/login.html | 11 ++-- translations/en.json | 24 ++++----- 21 files changed, 295 insertions(+), 107 deletions(-) create mode 100644 crates/storage-pg/.sqlx/query-1f131aa966a4358d83e7247d3e30451f8bcf5df20faf46a4a4c0d4a36d1ff173.json rename crates/storage-pg/.sqlx/{query-b44e77ba737c9ec9af3838f148e2e882c90c0118ff77a92d2d93fe97dbd33233.json => query-2098ff49e9f4b847543b3186dbc4a8cdf1d21f788a533477c233455c57a1b755.json} (67%) delete mode 100644 crates/storage-pg/.sqlx/query-311957a0b745660aa2a21b1bd211376739318efa1e84670e04189e1257d4a8ed.json create mode 100644 crates/storage-pg/.sqlx/query-4668abf6520ecca2fa71a26b02d206600624bbba57985d4a7fba2763478cd065.json delete mode 100644 crates/storage-pg/.sqlx/query-75b58c1b7f4e26997e961ad64418938938f09b3215a9b14f7edb3dd91cdf2dd5.json rename crates/storage-pg/.sqlx/{query-e1759a6bda20a09a423e9dcb3a7544dbf259fea54e7cdaa714455f05814f39f6.json => query-eb5af2b52b65eb9fba94418075dac0ddb04752d374e1905d1d935313090375c9.json} (66%) create mode 100644 crates/storage-pg/migrations/20231120110559_upstream_oauth_branding.sql create mode 100644 templates/components/idp_brand.html diff --git a/crates/cli/src/commands/config.rs b/crates/cli/src/commands/config.rs index 97e15ae53..5030cf2cb 100644 --- a/crates/cli/src/commands/config.rs +++ b/crates/cli/src/commands/config.rs @@ -267,6 +267,8 @@ async fn sync(root: &super::Options, prune: bool, dry_run: bool) -> anyhow::Resu provider.id, UpstreamOAuthProviderParams { issuer: provider.issuer, + human_name: provider.human_name, + brand_name: provider.brand_name, scope: provider.scope.parse()?, token_endpoint_auth_method, token_endpoint_signing_alg, diff --git a/crates/config/src/sections/upstream_oauth2.rs b/crates/config/src/sections/upstream_oauth2.rs index fed8a6d7f..94ae31fe5 100644 --- a/crates/config/src/sections/upstream_oauth2.rs +++ b/crates/config/src/sections/upstream_oauth2.rs @@ -245,6 +245,22 @@ pub struct Provider { /// The OIDC issuer URL pub issuer: String, + /// A human-readable name for the provider, that will be shown to users + pub human_name: Option, + + /// A brand identifier used to customise the UI, e.g. `apple`, `google`, + /// `github`, etc. + /// + /// Values supported by the default template are: + /// + /// - `apple` + /// - `google` + /// - `facebook` + /// - `github` + /// - `gitlab` + /// - `twitter` + pub brand_name: Option, + /// The client ID to use when authenticating with the provider pub client_id: String, diff --git a/crates/data-model/src/upstream_oauth2/provider.rs b/crates/data-model/src/upstream_oauth2/provider.rs index 3bc397b2a..e9aea4496 100644 --- a/crates/data-model/src/upstream_oauth2/provider.rs +++ b/crates/data-model/src/upstream_oauth2/provider.rs @@ -128,6 +128,8 @@ impl std::fmt::Display for PkceMode { pub struct UpstreamOAuthProvider { pub id: Ulid, pub issuer: String, + pub human_name: Option, + pub brand_name: Option, pub discovery_mode: DiscoveryMode, pub pkce_mode: PkceMode, pub jwks_uri_override: Option, diff --git a/crates/handlers/src/upstream_oauth2/cache.rs b/crates/handlers/src/upstream_oauth2/cache.rs index d7a97cd7c..cf10280a0 100644 --- a/crates/handlers/src/upstream_oauth2/cache.rs +++ b/crates/handlers/src/upstream_oauth2/cache.rs @@ -491,6 +491,8 @@ mod tests { let provider = UpstreamOAuthProvider { id: Ulid::nil(), issuer: "https://valid.example.com/".to_owned(), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, discovery_mode: UpstreamOAuthProviderDiscoveryMode::Oidc, pkce_mode: UpstreamOAuthProviderPkceMode::Auto, jwks_uri_override: None, diff --git a/crates/handlers/src/upstream_oauth2/link.rs b/crates/handlers/src/upstream_oauth2/link.rs index 84b3ebb77..0906b49d1 100644 --- a/crates/handlers/src/upstream_oauth2/link.rs +++ b/crates/handlers/src/upstream_oauth2/link.rs @@ -861,6 +861,8 @@ mod tests { &state.clock, UpstreamOAuthProviderParams { issuer: "https://example.com/".to_owned(), + human_name: Some("Example Ltd.".to_owned()), + brand_name: None, scope: Scope::from_iter([OPENID]), token_endpoint_auth_method: OAuthClientAuthenticationMethod::None, token_endpoint_signing_alg: None, diff --git a/crates/handlers/src/views/login.rs b/crates/handlers/src/views/login.rs index 764d05c12..d0b088a2a 100644 --- a/crates/handlers/src/views/login.rs +++ b/crates/handlers/src/views/login.rs @@ -351,6 +351,8 @@ mod test { &state.clock, UpstreamOAuthProviderParams { issuer: "https://first.com/".to_owned(), + human_name: Some("First Ltd.".to_owned()), + brand_name: None, scope: [OPENID].into_iter().collect(), token_endpoint_auth_method: OAuthClientAuthenticationMethod::None, token_endpoint_signing_alg: None, @@ -383,6 +385,8 @@ mod test { &state.clock, UpstreamOAuthProviderParams { issuer: "https://second.com/".to_owned(), + human_name: Some("Second Ltd.".to_owned()), + brand_name: None, scope: [OPENID].into_iter().collect(), token_endpoint_auth_method: OAuthClientAuthenticationMethod::None, token_endpoint_signing_alg: None, @@ -405,11 +409,11 @@ mod test { let response = state.request(Request::get("/login").empty()).await; response.assert_status(StatusCode::OK); response.assert_header_value(CONTENT_TYPE, "text/html; charset=utf-8"); - assert!(response.body().contains(&escape_html("first.com/"))); + assert!(response.body().contains(&escape_html("First Ltd."))); assert!(response .body() .contains(&escape_html(&first_provider_login.path_and_query()))); - assert!(response.body().contains(&escape_html("second.com/"))); + assert!(response.body().contains(&escape_html("Second Ltd."))); assert!(response .body() .contains(&escape_html(&second_provider_login.path_and_query()))); diff --git a/crates/storage-pg/.sqlx/query-1f131aa966a4358d83e7247d3e30451f8bcf5df20faf46a4a4c0d4a36d1ff173.json b/crates/storage-pg/.sqlx/query-1f131aa966a4358d83e7247d3e30451f8bcf5df20faf46a4a4c0d4a36d1ff173.json new file mode 100644 index 000000000..2a92e950c --- /dev/null +++ b/crates/storage-pg/.sqlx/query-1f131aa966a4358d83e7247d3e30451f8bcf5df20faf46a4a4c0d4a36d1ff173.json @@ -0,0 +1,29 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14, $15, $16)\n ", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Jsonb", + "Text", + "Text", + "Text", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [] + }, + "hash": "1f131aa966a4358d83e7247d3e30451f8bcf5df20faf46a4a4c0d4a36d1ff173" +} diff --git a/crates/storage-pg/.sqlx/query-b44e77ba737c9ec9af3838f148e2e882c90c0118ff77a92d2d93fe97dbd33233.json b/crates/storage-pg/.sqlx/query-2098ff49e9f4b847543b3186dbc4a8cdf1d21f788a533477c233455c57a1b755.json similarity index 67% rename from crates/storage-pg/.sqlx/query-b44e77ba737c9ec9af3838f148e2e882c90c0118ff77a92d2d93fe97dbd33233.json rename to crates/storage-pg/.sqlx/query-2098ff49e9f4b847543b3186dbc4a8cdf1d21f788a533477c233455c57a1b755.json index 3a4882d05..06940c1a3 100644 --- a/crates/storage-pg/.sqlx/query-b44e77ba737c9ec9af3838f148e2e882c90c0118ff77a92d2d93fe97dbd33233.json +++ b/crates/storage-pg/.sqlx/query-2098ff49e9f4b847543b3186dbc4a8cdf1d21f788a533477c233455c57a1b755.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n discovery_mode,\n pkce_mode\n FROM upstream_oauth_providers\n ", + "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n discovery_mode,\n pkce_mode\n FROM upstream_oauth_providers\n ", "describe": { "columns": [ { @@ -15,61 +15,71 @@ }, { "ordinal": 2, - "name": "scope", + "name": "human_name", "type_info": "Text" }, { "ordinal": 3, - "name": "client_id", + "name": "brand_name", "type_info": "Text" }, { "ordinal": 4, - "name": "encrypted_client_secret", + "name": "scope", "type_info": "Text" }, { "ordinal": 5, - "name": "token_endpoint_signing_alg", + "name": "client_id", "type_info": "Text" }, { "ordinal": 6, - "name": "token_endpoint_auth_method", + "name": "encrypted_client_secret", "type_info": "Text" }, { "ordinal": 7, + "name": "token_endpoint_signing_alg", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "token_endpoint_auth_method", + "type_info": "Text" + }, + { + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 10, "name": "claims_imports: Json", "type_info": "Jsonb" }, { - "ordinal": 9, + "ordinal": 11, "name": "jwks_uri_override", "type_info": "Text" }, { - "ordinal": 10, + "ordinal": 12, "name": "authorization_endpoint_override", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 13, "name": "token_endpoint_override", "type_info": "Text" }, { - "ordinal": 12, + "ordinal": 14, "name": "discovery_mode", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 15, "name": "pkce_mode", "type_info": "Text" } @@ -80,6 +90,8 @@ "nullable": [ false, false, + true, + true, false, false, true, @@ -94,5 +106,5 @@ false ] }, - "hash": "b44e77ba737c9ec9af3838f148e2e882c90c0118ff77a92d2d93fe97dbd33233" + "hash": "2098ff49e9f4b847543b3186dbc4a8cdf1d21f788a533477c233455c57a1b755" } diff --git a/crates/storage-pg/.sqlx/query-311957a0b745660aa2a21b1bd211376739318efa1e84670e04189e1257d4a8ed.json b/crates/storage-pg/.sqlx/query-311957a0b745660aa2a21b1bd211376739318efa1e84670e04189e1257d4a8ed.json deleted file mode 100644 index f2efcde7a..000000000 --- a/crates/storage-pg/.sqlx/query-311957a0b745660aa2a21b1bd211376739318efa1e84670e04189e1257d4a8ed.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14)\n ", - "describe": { - "columns": [], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Jsonb", - "Text", - "Text", - "Text", - "Text", - "Text", - "Timestamptz" - ] - }, - "nullable": [] - }, - "hash": "311957a0b745660aa2a21b1bd211376739318efa1e84670e04189e1257d4a8ed" -} diff --git a/crates/storage-pg/.sqlx/query-4668abf6520ecca2fa71a26b02d206600624bbba57985d4a7fba2763478cd065.json b/crates/storage-pg/.sqlx/query-4668abf6520ecca2fa71a26b02d206600624bbba57985d4a7fba2763478cd065.json new file mode 100644 index 000000000..0752d5a6d --- /dev/null +++ b/crates/storage-pg/.sqlx/query-4668abf6520ecca2fa71a26b02d206600624bbba57985d4a7fba2763478cd065.json @@ -0,0 +1,37 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14, $15, $16)\n ON CONFLICT (upstream_oauth_provider_id) \n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n human_name = EXCLUDED.human_name,\n brand_name = EXCLUDED.brand_name,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode\n RETURNING created_at\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Text", + "Jsonb", + "Text", + "Text", + "Text", + "Text", + "Text", + "Timestamptz" + ] + }, + "nullable": [ + false + ] + }, + "hash": "4668abf6520ecca2fa71a26b02d206600624bbba57985d4a7fba2763478cd065" +} diff --git a/crates/storage-pg/.sqlx/query-75b58c1b7f4e26997e961ad64418938938f09b3215a9b14f7edb3dd91cdf2dd5.json b/crates/storage-pg/.sqlx/query-75b58c1b7f4e26997e961ad64418938938f09b3215a9b14f7edb3dd91cdf2dd5.json deleted file mode 100644 index da2027bf2..000000000 --- a/crates/storage-pg/.sqlx/query-75b58c1b7f4e26997e961ad64418938938f09b3215a9b14f7edb3dd91cdf2dd5.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n INSERT INTO upstream_oauth_providers (\n upstream_oauth_provider_id,\n issuer,\n scope,\n token_endpoint_auth_method,\n token_endpoint_signing_alg,\n client_id,\n encrypted_client_secret,\n claims_imports,\n authorization_endpoint_override,\n token_endpoint_override,\n jwks_uri_override,\n discovery_mode,\n pkce_mode,\n created_at\n ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9,\n $10, $11, $12, $13, $14)\n ON CONFLICT (upstream_oauth_provider_id) \n DO UPDATE\n SET\n issuer = EXCLUDED.issuer,\n scope = EXCLUDED.scope,\n token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method,\n token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg,\n client_id = EXCLUDED.client_id,\n encrypted_client_secret = EXCLUDED.encrypted_client_secret,\n claims_imports = EXCLUDED.claims_imports,\n authorization_endpoint_override = EXCLUDED.authorization_endpoint_override,\n token_endpoint_override = EXCLUDED.token_endpoint_override,\n jwks_uri_override = EXCLUDED.jwks_uri_override,\n discovery_mode = EXCLUDED.discovery_mode,\n pkce_mode = EXCLUDED.pkce_mode\n RETURNING created_at\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "created_at", - "type_info": "Timestamptz" - } - ], - "parameters": { - "Left": [ - "Uuid", - "Text", - "Text", - "Text", - "Text", - "Text", - "Text", - "Jsonb", - "Text", - "Text", - "Text", - "Text", - "Text", - "Timestamptz" - ] - }, - "nullable": [ - false - ] - }, - "hash": "75b58c1b7f4e26997e961ad64418938938f09b3215a9b14f7edb3dd91cdf2dd5" -} diff --git a/crates/storage-pg/.sqlx/query-e1759a6bda20a09a423e9dcb3a7544dbf259fea54e7cdaa714455f05814f39f6.json b/crates/storage-pg/.sqlx/query-eb5af2b52b65eb9fba94418075dac0ddb04752d374e1905d1d935313090375c9.json similarity index 66% rename from crates/storage-pg/.sqlx/query-e1759a6bda20a09a423e9dcb3a7544dbf259fea54e7cdaa714455f05814f39f6.json rename to crates/storage-pg/.sqlx/query-eb5af2b52b65eb9fba94418075dac0ddb04752d374e1905d1d935313090375c9.json index 90fec18d8..f7e51556d 100644 --- a/crates/storage-pg/.sqlx/query-e1759a6bda20a09a423e9dcb3a7544dbf259fea54e7cdaa714455f05814f39f6.json +++ b/crates/storage-pg/.sqlx/query-eb5af2b52b65eb9fba94418075dac0ddb04752d374e1905d1d935313090375c9.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n discovery_mode,\n pkce_mode\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ", + "query": "\n SELECT\n upstream_oauth_provider_id,\n issuer,\n human_name,\n brand_name,\n scope,\n client_id,\n encrypted_client_secret,\n token_endpoint_signing_alg,\n token_endpoint_auth_method,\n created_at,\n claims_imports as \"claims_imports: Json\",\n jwks_uri_override,\n authorization_endpoint_override,\n token_endpoint_override,\n discovery_mode,\n pkce_mode\n FROM upstream_oauth_providers\n WHERE upstream_oauth_provider_id = $1\n ", "describe": { "columns": [ { @@ -15,61 +15,71 @@ }, { "ordinal": 2, - "name": "scope", + "name": "human_name", "type_info": "Text" }, { "ordinal": 3, - "name": "client_id", + "name": "brand_name", "type_info": "Text" }, { "ordinal": 4, - "name": "encrypted_client_secret", + "name": "scope", "type_info": "Text" }, { "ordinal": 5, - "name": "token_endpoint_signing_alg", + "name": "client_id", "type_info": "Text" }, { "ordinal": 6, - "name": "token_endpoint_auth_method", + "name": "encrypted_client_secret", "type_info": "Text" }, { "ordinal": 7, + "name": "token_endpoint_signing_alg", + "type_info": "Text" + }, + { + "ordinal": 8, + "name": "token_endpoint_auth_method", + "type_info": "Text" + }, + { + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 10, "name": "claims_imports: Json", "type_info": "Jsonb" }, { - "ordinal": 9, + "ordinal": 11, "name": "jwks_uri_override", "type_info": "Text" }, { - "ordinal": 10, + "ordinal": 12, "name": "authorization_endpoint_override", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 13, "name": "token_endpoint_override", "type_info": "Text" }, { - "ordinal": 12, + "ordinal": 14, "name": "discovery_mode", "type_info": "Text" }, { - "ordinal": 13, + "ordinal": 15, "name": "pkce_mode", "type_info": "Text" } @@ -82,6 +92,8 @@ "nullable": [ false, false, + true, + true, false, false, true, @@ -96,5 +108,5 @@ false ] }, - "hash": "e1759a6bda20a09a423e9dcb3a7544dbf259fea54e7cdaa714455f05814f39f6" + "hash": "eb5af2b52b65eb9fba94418075dac0ddb04752d374e1905d1d935313090375c9" } diff --git a/crates/storage-pg/migrations/20231120110559_upstream_oauth_branding.sql b/crates/storage-pg/migrations/20231120110559_upstream_oauth_branding.sql new file mode 100644 index 000000000..1f54c86f8 --- /dev/null +++ b/crates/storage-pg/migrations/20231120110559_upstream_oauth_branding.sql @@ -0,0 +1,18 @@ +-- Copyright 2023 The Matrix.org Foundation C.I.C. +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Adds human readable branding information to the upstream_oauth_providers table +ALTER TABLE upstream_oauth_providers + ADD COLUMN human_name text, + ADD COLUMN brand_name text; diff --git a/crates/storage-pg/src/iden.rs b/crates/storage-pg/src/iden.rs index f07e8151e..b21cd849a 100644 --- a/crates/storage-pg/src/iden.rs +++ b/crates/storage-pg/src/iden.rs @@ -96,6 +96,8 @@ pub enum UpstreamOAuthProviders { #[iden = "upstream_oauth_provider_id"] UpstreamOAuthProviderId, Issuer, + HumanName, + BrandName, Scope, ClientId, EncryptedClientSecret, diff --git a/crates/storage-pg/src/upstream_oauth2/mod.rs b/crates/storage-pg/src/upstream_oauth2/mod.rs index 0e34726e8..2876fe55a 100644 --- a/crates/storage-pg/src/upstream_oauth2/mod.rs +++ b/crates/storage-pg/src/upstream_oauth2/mod.rs @@ -62,6 +62,8 @@ mod tests { &clock, UpstreamOAuthProviderParams { issuer: "https://example.com/".to_owned(), + human_name: None, + brand_name: None, scope: Scope::from_iter([OPENID]), token_endpoint_auth_method: mas_iana::oauth::OAuthClientAuthenticationMethod::None, @@ -243,6 +245,8 @@ mod tests { &clock, UpstreamOAuthProviderParams { issuer: ISSUER.to_owned(), + human_name: None, + brand_name: None, scope: scope.clone(), token_endpoint_auth_method: mas_iana::oauth::OAuthClientAuthenticationMethod::None, diff --git a/crates/storage-pg/src/upstream_oauth2/provider.rs b/crates/storage-pg/src/upstream_oauth2/provider.rs index 5b991e54e..8f66a1e0b 100644 --- a/crates/storage-pg/src/upstream_oauth2/provider.rs +++ b/crates/storage-pg/src/upstream_oauth2/provider.rs @@ -53,6 +53,8 @@ impl<'c> PgUpstreamOAuthProviderRepository<'c> { struct ProviderLookup { upstream_oauth_provider_id: Uuid, issuer: String, + human_name: Option, + brand_name: Option, scope: String, client_id: String, encrypted_client_secret: Option, @@ -144,6 +146,8 @@ impl TryFrom for UpstreamOAuthProvider { Ok(UpstreamOAuthProvider { id, issuer: value.issuer, + human_name: value.human_name, + brand_name: value.brand_name, scope, client_id: value.client_id, encrypted_client_secret: value.encrypted_client_secret, @@ -180,6 +184,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' SELECT upstream_oauth_provider_id, issuer, + human_name, + brand_name, scope, client_id, encrypted_client_secret, @@ -235,6 +241,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' INSERT INTO upstream_oauth_providers ( upstream_oauth_provider_id, issuer, + human_name, + brand_name, scope, token_endpoint_auth_method, token_endpoint_signing_alg, @@ -248,10 +256,12 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' pkce_mode, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, - $10, $11, $12, $13, $14) + $10, $11, $12, $13, $14, $15, $16) "#, Uuid::from(id), ¶ms.issuer, + params.human_name.as_deref(), + params.brand_name.as_deref(), params.scope.to_string(), params.token_endpoint_auth_method.to_string(), params @@ -281,6 +291,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' Ok(UpstreamOAuthProvider { id, issuer: params.issuer, + human_name: params.human_name, + brand_name: params.brand_name, scope: params.scope, client_id: params.client_id, encrypted_client_secret: params.encrypted_client_secret, @@ -386,6 +398,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' INSERT INTO upstream_oauth_providers ( upstream_oauth_provider_id, issuer, + human_name, + brand_name, scope, token_endpoint_auth_method, token_endpoint_signing_alg, @@ -399,11 +413,13 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' pkce_mode, created_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, - $10, $11, $12, $13, $14) + $10, $11, $12, $13, $14, $15, $16) ON CONFLICT (upstream_oauth_provider_id) DO UPDATE SET issuer = EXCLUDED.issuer, + human_name = EXCLUDED.human_name, + brand_name = EXCLUDED.brand_name, scope = EXCLUDED.scope, token_endpoint_auth_method = EXCLUDED.token_endpoint_auth_method, token_endpoint_signing_alg = EXCLUDED.token_endpoint_signing_alg, @@ -419,6 +435,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' "#, Uuid::from(id), ¶ms.issuer, + params.human_name.as_deref(), + params.brand_name.as_deref(), params.scope.to_string(), params.token_endpoint_auth_method.to_string(), params @@ -448,6 +466,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' Ok(UpstreamOAuthProvider { id, issuer: params.issuer, + human_name: params.human_name, + brand_name: params.brand_name, scope: params.scope, client_id: params.client_id, encrypted_client_secret: params.encrypted_client_secret, @@ -492,6 +512,20 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' )), ProviderLookupIden::Issuer, ) + .expr_as( + Expr::col(( + UpstreamOAuthProviders::Table, + UpstreamOAuthProviders::HumanName, + )), + ProviderLookupIden::HumanName, + ) + .expr_as( + Expr::col(( + UpstreamOAuthProviders::Table, + UpstreamOAuthProviders::BrandName, + )), + ProviderLookupIden::BrandName, + ) .expr_as( Expr::col((UpstreamOAuthProviders::Table, UpstreamOAuthProviders::Scope)), ProviderLookupIden::Scope, @@ -644,6 +678,8 @@ impl<'c> UpstreamOAuthProviderRepository for PgUpstreamOAuthProviderRepository<' SELECT upstream_oauth_provider_id, issuer, + human_name, + brand_name, scope, client_id, encrypted_client_secret, diff --git a/crates/storage/src/upstream_oauth2/provider.rs b/crates/storage/src/upstream_oauth2/provider.rs index 3a91994e0..5de174aef 100644 --- a/crates/storage/src/upstream_oauth2/provider.rs +++ b/crates/storage/src/upstream_oauth2/provider.rs @@ -33,6 +33,12 @@ pub struct UpstreamOAuthProviderParams { /// The OIDC issuer of the provider pub issuer: String, + /// A human-readable name for the provider + pub human_name: Option, + + /// A brand identifier, e.g. "apple" or "google" + pub brand_name: Option, + /// The scope to request during the authorization flow pub scope: Scope, diff --git a/docs/config.schema.json b/docs/config.schema.json index 43b42a9b0..2846e2282 100644 --- a/docs/config.schema.json +++ b/docs/config.schema.json @@ -1763,6 +1763,10 @@ "type": "string", "format": "uri" }, + "brand_name": { + "description": "A brand identifier used to customise the UI, e.g. `apple`, `google`, `github`, etc.\n\nValues supported by the default template are:\n\n- `apple` - `google` - `facebook` - `github` - `gitlab` - `twitter`", + "type": "string" + }, "claims_imports": { "description": "How claims should be imported from the `id_token` provided by the provider", "allOf": [ @@ -1784,6 +1788,10 @@ } ] }, + "human_name": { + "description": "A human-readable name for the provider, that will be shown to users", + "type": "string" + }, "id": { "description": "A ULID as per https://github.com/ulid/spec", "type": "string", diff --git a/templates/components/idp_brand.html b/templates/components/idp_brand.html new file mode 100644 index 000000000..8891bd678 --- /dev/null +++ b/templates/components/idp_brand.html @@ -0,0 +1,53 @@ +{# +Copyright 2023 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +#} + +{% macro logo(brand, class="") -%} + {% if brand == "google" -%} + + + + + + + {% elif brand == "gitlab" %} + + + + + + + + + + {% elif brand == "twitter" %} + + + + {% elif brand == "github" %} + + + + {% elif brand == "facebook" %} + + + + + {% elif brand == "apple" %} + + + + {% endif %} +{% endmacro %} diff --git a/templates/pages/login.html b/templates/pages/login.html index e2f0fec22..fc23b4607 100644 --- a/templates/pages/login.html +++ b/templates/pages/login.html @@ -16,6 +16,8 @@ {% extends "base.html" %} +{% from "components/idp_brand.html" import logo %} + {% block content %}
{% if not password_disabled %} @@ -27,7 +29,8 @@ {% if next and next.kind == "link_upstream" %}

{{ _("mas.login.link.headline") }}

-

{{ _("mas.login.link.description", provider=next.provider.issuer) }}

+ {% set name = provider.human_name | default(provider.issuer | simplify_url(keep_path=True)) | default(provider.id) %} +

{{ _("mas.login.link.description", provider=name) }}

{% else %}
@@ -78,8 +81,10 @@

{{ _("mas.login.headline") }}

{% set params = next["params"] | default({}) | to_params(prefix="?") %} {% for provider in providers %} - - {{ _("mas.login.continue_with_provider", provider=provider.issuer | simplify_url(keep_path=True)) }} + {% set name = provider.human_name | default(provider.issuer | simplify_url(keep_path=True)) | default(provider.id) %} + + {{ logo(provider.brand_name) }} + {{ _("mas.login.continue_with_provider", provider=name) }} {% endfor %} {% endif %} diff --git a/translations/en.json b/translations/en.json index a3dba79bc..482696c74 100644 --- a/translations/en.json +++ b/translations/en.json @@ -2,15 +2,15 @@ "action": { "cancel": "Cancel", "@cancel": { - "context": "pages/consent.html:72:11-29, pages/login.html:95:13-31, pages/policy_violation.html:50:11-29, pages/register.html:64:13-31" + "context": "pages/consent.html:72:11-29, pages/login.html:100:13-31, pages/policy_violation.html:50:11-29, pages/register.html:64:13-31" }, "continue": "Continue", "@continue": { - "context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/login.html:59:30-50, pages/reauth.html:40:28-48, pages/register.html:59:28-48, pages/sso.html:45:28-48" + "context": "pages/account/emails/add.html:45:26-46, pages/account/emails/verify.html:60:26-46, pages/consent.html:60:28-48, pages/login.html:62:30-50, pages/reauth.html:40:28-48, pages/register.html:59:28-48, pages/sso.html:45:28-48" }, "create_account": "Create Account", "@create_account": { - "context": "pages/login.html:69:35-61, pages/upstream_oauth2/do_register.html:143:26-52" + "context": "pages/login.html:72:35-61, pages/upstream_oauth2/do_register.html:143:26-52" }, "sign_in": "Sign in", "@sign_in": { @@ -75,7 +75,7 @@ }, "password": "Password", "@password": { - "context": "pages/login.html:55:37-57, pages/reauth.html:36:35-55, pages/register.html:51:35-55" + "context": "pages/login.html:58:37-57, pages/reauth.html:36:35-55, pages/register.html:51:35-55" }, "password_confirm": "Confirm password", "@password_confirm": { @@ -83,7 +83,7 @@ }, "username": "Username", "@username": { - "context": "pages/login.html:51:37-57, pages/register.html:43:35-55, pages/upstream_oauth2/do_register.html:74:35-55, pages/upstream_oauth2/do_register.html:79:39-59" + "context": "pages/login.html:54:37-57, pages/register.html:43:35-55, pages/upstream_oauth2/do_register.html:74:35-55, pages/upstream_oauth2/do_register.html:79:39-59" } }, "error": { @@ -189,34 +189,34 @@ "login": { "call_to_register": "Don't have an account yet?", "@call_to_register": { - "context": "pages/login.html:65:15-46" + "context": "pages/login.html:68:15-46" }, "continue_with_provider": "Continue with %(provider)s", "@continue_with_provider": { - "context": "pages/login.html:82:13-107", + "context": "pages/login.html:87:13-65", "description": "Button to log in with an upstream provider" }, "description": "Please sign in to continue:", "@description": { - "context": "pages/login.html:35:31-57" + "context": "pages/login.html:38:31-57" }, "headline": "Sign in", "@headline": { - "context": "pages/login.html:34:33-56" + "context": "pages/login.html:37:33-56" }, "link": { "description": "Linking your %(provider)s account", "@description": { - "context": "pages/login.html:30:31-93" + "context": "pages/login.html:33:31-77" }, "headline": "Sign in to link", "@headline": { - "context": "pages/login.html:29:33-61" + "context": "pages/login.html:31:33-61" } }, "no_login_methods": "No login methods available.", "@no_login_methods": { - "context": "pages/login.html:89:11-42" + "context": "pages/login.html:94:11-42" } }, "navbar": {