diff --git a/.github/scripts/setup-keycloak.sh b/.github/scripts/setup-keycloak.sh index 773353db91..7d2c4e3d52 100755 --- a/.github/scripts/setup-keycloak.sh +++ b/.github/scripts/setup-keycloak.sh @@ -1,6 +1,6 @@ #!/bin/bash -export KC_VERSION=25.0.1 +export KC_VERSION=26.2.5 curl -LO https://github.com/keycloak/keycloak/releases/download/"${KC_VERSION}"/keycloak-"${KC_VERSION}".zip unzip -q keycloak-${KC_VERSION}.zip diff --git a/controlplane/.env.example b/controlplane/.env.example index 7715590f71..d8a3279427 100644 --- a/controlplane/.env.example +++ b/controlplane/.env.example @@ -16,6 +16,7 @@ CLICKHOUSE_MIGRATION_DSN="clickhouse://default:changeme@localhost:9000?database= # Security AUTH_JWT_SECRET="fkczyomvdprgvtmvkuhvprxuggkbgwld" +AUTH_SSO_COOKIE_DOMAIN= # Keycloak KC_REALM="cosmo" diff --git a/controlplane/package.json b/controlplane/package.json index 9065204d2a..49f8c5e1fe 100644 --- a/controlplane/package.json +++ b/controlplane/package.json @@ -49,7 +49,7 @@ "@graphql-eslint/eslint-plugin": "^3.20.1", "@graphql-inspector/core": "^6.2.1", "@graphql-tools/utils": "^10.1.2", - "@keycloak/keycloak-admin-client": "^25.0.2", + "@keycloak/keycloak-admin-client": "26.2.5", "@octokit/webhooks-types": "^7.6.1", "@sentry/node": "^10.11.0", "@sentry/node-native": "^10.11.0", diff --git a/controlplane/src/core/auth-utils.ts b/controlplane/src/core/auth-utils.ts index 63a49a838a..c6a9439e29 100644 --- a/controlplane/src/core/auth-utils.ts +++ b/controlplane/src/core/auth-utils.ts @@ -21,6 +21,7 @@ import { AuthenticationError } from './errors/errors.js'; export type AuthUtilsOptions = { webBaseUrl: string; webErrorPath: string; + ssoCookieDomain: string | undefined; jwtSecret: string; oauth: { clientID: string; @@ -113,7 +114,7 @@ export default class AuthUtils { createSsoCookie(res: FastifyReply, ssoSlug: string) { const currentDate = new Date(); const userSsoCookie = cookie.serialize(cosmoIdpHintCookieName, ssoSlug, { - domain: this.webDomain, + domain: this.opts.ssoCookieDomain ?? this.webDomain, sameSite: 'lax', expires: new Date(currentDate.setFullYear(currentDate.getFullYear() + 1)), path: '/', diff --git a/controlplane/src/core/build-server.ts b/controlplane/src/core/build-server.ts index 0820536ae5..4ecb4e8173 100644 --- a/controlplane/src/core/build-server.ts +++ b/controlplane/src/core/build-server.ts @@ -82,6 +82,7 @@ export interface BuildConfig { auth: { webBaseUrl: string; secureCookie?: boolean; + ssoCookieDomain?: string; webErrorPath: string; secret: string; redirectUri: string; @@ -236,6 +237,7 @@ export default async function build(opts: BuildConfig) { cookieName: pkceCodeVerifierCookieName, }, webBaseUrl: opts.auth.webBaseUrl, + ssoCookieDomain: opts.auth.ssoCookieDomain, webErrorPath: opts.auth.webErrorPath, }); diff --git a/controlplane/src/core/env.schema.ts b/controlplane/src/core/env.schema.ts index 8bcb78b331..812b8836f2 100644 --- a/controlplane/src/core/env.schema.ts +++ b/controlplane/src/core/env.schema.ts @@ -57,6 +57,14 @@ export const envVariables = z * Auth */ AUTH_JWT_SECRET: z.string().min(32).max(32), + AUTH_SSO_COOKIE_DOMAIN: z + .string() + .transform((val) => (val?.trim() === '' ? undefined : val)) + .optional() + .refine( + (val) => !val || /^[\d.a-z-]+$/i.test(val), + 'AUTH_SSO_COOKIE_DOMAIN must be a valid domain (e.g. ".example.com")', + ), AUTH_REDIRECT_URI: z.string().url(), /** * Database diff --git a/controlplane/src/index.ts b/controlplane/src/index.ts index 4c1c64ea58..a0ec5198e3 100644 --- a/controlplane/src/index.ts +++ b/controlplane/src/index.ts @@ -25,6 +25,7 @@ const { AUTH_REDIRECT_URI, WEB_BASE_URL, AUTH_JWT_SECRET, + AUTH_SSO_COOKIE_DOMAIN, KC_REALM, KC_LOGIN_REALM, KC_CLIENT_ID, @@ -108,6 +109,7 @@ const options: BuildConfig = { secret: AUTH_JWT_SECRET, webBaseUrl: WEB_BASE_URL, webErrorPath: '/auth/error', + ssoCookieDomain: AUTH_SSO_COOKIE_DOMAIN, }, webhook: { url: WEBHOOK_URL, diff --git a/docker/keycloak/realm.json b/docker/keycloak/realm.json index b6b6b13e3b..839ef0d505 100644 --- a/docker/keycloak/realm.json +++ b/docker/keycloak/realm.json @@ -39,6 +39,8 @@ "editUsernameAllowed": false, "bruteForceProtected": true, "permanentLockout": false, + "maxTemporaryLockouts": 0, + "bruteForceStrategy": "MULTIPLE", "maxFailureWaitSeconds": 3600, "minimumQuickLoginWaitSeconds": 60, "waitIncrementSeconds": 60, @@ -53,7 +55,9 @@ "clientRole": false, "containerId": "b6c8b6d7-7435-46d9-833a-d088cfc6f993" }, - "requiredCredentials": ["password"], + "requiredCredentials": [ + "password" + ], "passwordPolicy": "length(8) and specialChars(1) and maxLength(60) and digits(1) and notUsername(undefined)", "otpPolicyType": "totp", "otpPolicyAlgorithm": "HmacSHA1", @@ -62,9 +66,16 @@ "otpPolicyLookAheadWindow": 1, "otpPolicyPeriod": 30, "otpPolicyCodeReusable": false, - "otpSupportedApplications": ["totpAppMicrosoftAuthenticatorName", "totpAppFreeOTPName", "totpAppGoogleName"], + "otpSupportedApplications": [ + "totpAppFreeOTPName", + "totpAppGoogleName", + "totpAppMicrosoftAuthenticatorName" + ], + "localizationTexts": {}, "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": ["ES256"], + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], "webAuthnPolicyRpId": "", "webAuthnPolicyAttestationConveyancePreference": "not specified", "webAuthnPolicyAuthenticatorAttachment": "not specified", @@ -73,8 +84,11 @@ "webAuthnPolicyCreateTimeout": 0, "webAuthnPolicyAvoidSameAuthenticatorRegister": false, "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyExtraOrigins": [], "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": ["ES256"], + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], "webAuthnPolicyPasswordlessRpId": "", "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", @@ -83,17 +97,23 @@ "webAuthnPolicyPasswordlessCreateTimeout": 0, "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "webAuthnPolicyPasswordlessExtraOrigins": [], "scopeMappings": [ { "clientScope": "offline_access", - "roles": ["offline_access"] + "roles": [ + "offline_access" + ] } ], "clientScopeMappings": { "account": [ { "client": "account-console", - "roles": ["manage-account", "view-groups"] + "roles": [ + "manage-account", + "view-groups" + ] } ] }, @@ -108,7 +128,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/cosmo/account/*"], + "redirectUris": [ + "/realms/cosmo/account/*" + ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, @@ -121,13 +143,26 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "false", "post.logout.redirect.uris": "+" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "e67441e6-606b-42ce-98b5-4e4c3df2906e", @@ -139,7 +174,9 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/realms/cosmo/account/*"], + "redirectUris": [ + "/realms/cosmo/account/*" + ], "webOrigins": [], "notBefore": 0, "bearerOnly": false, @@ -152,6 +189,7 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "false", "post.logout.redirect.uris": "+", "pkce.code.challenge.method": "S256" }, @@ -168,8 +206,20 @@ "config": {} } ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "d028c73f-ebc4-4654-982e-7efe362f2127", @@ -192,13 +242,27 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", "post.logout.redirect.uris": "+" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "184984e3-f712-46c6-9f03-4fdb43f1a804", @@ -221,13 +285,25 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "true", "post.logout.redirect.uris": "+" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "393fd573-85eb-42b5-bbb5-da1417f41ced", @@ -241,8 +317,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/*"], - "webOrigins": [""], + "redirectUris": [ + "/*" + ], + "webOrigins": [ + "" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -255,10 +335,14 @@ "protocol": "openid-connect", "attributes": { "client.secret.creation.time": "1695137206", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "post.logout.redirect.uris": "+", "oauth2.device.authorization.grant.enabled": "true", "backchannel.logout.revoke.offline.tokens": "false", "use.refresh.tokens": "true", + "realm_client": "false", "oidc.ciba.grant.enabled": "false", + "client.use.lightweight.access.token.enabled": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", "acr.loa.map": "{}", @@ -267,7 +351,9 @@ "display.on.consent.screen": "false", "token.response.type.bearer.lower-case": "false" }, - "authenticationFlowBindingOverrides": {}, + "authenticationFlowBindingOverrides": { + "browser": "247d2578-faee-4b43-a37c-b222e96d9f5b" + }, "fullScopeAllowed": true, "nodeReRegistrationTimeout": -1, "protocolMappers": [ @@ -279,16 +365,28 @@ "consentRequired": false, "config": { "full.path": "true", - "userinfo.token.claim": "true", - "multivalued": "true", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "groups" + "claim.name": "groups", + "userinfo.token.claim": "true", + "multivalued": "true" } } ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "af4725d5-756a-4063-962a-27f6f68a0e82", @@ -311,13 +409,25 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "true", "post.logout.redirect.uris": "+" }, "authenticationFlowBindingOverrides": {}, "fullScopeAllowed": false, "nodeReRegistrationTimeout": 0, - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "389e3d6d-5ce1-4727-beec-fddf82240cfc", @@ -329,8 +439,12 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["/admin/cosmo/console/*"], - "webOrigins": ["+"], + "redirectUris": [ + "/admin/cosmo/console/*" + ], + "webOrigins": [ + "+" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -342,6 +456,8 @@ "frontchannelLogout": false, "protocol": "openid-connect", "attributes": { + "realm_client": "false", + "client.use.lightweight.access.token.enabled": "true", "post.logout.redirect.uris": "+", "pkce.code.challenge.method": "S256" }, @@ -356,17 +472,29 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "locale", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "locale", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } } ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] }, { "id": "fcf397b9-57cc-4d80-9589-30fdb992a876", @@ -380,8 +508,14 @@ "enabled": true, "alwaysDisplayInConsole": false, "clientAuthenticatorType": "client-secret", - "redirectUris": ["http://localhost:3000/*", "http://localhost:3001/*"], - "webOrigins": ["", "http://localhost:3000"], + "redirectUris": [ + "http://localhost:3000/*", + "http://localhost:3001/*" + ], + "webOrigins": [ + "", + "http://localhost:3000" + ], "notBefore": 0, "bearerOnly": false, "consentRequired": false, @@ -393,17 +527,27 @@ "frontchannelLogout": true, "protocol": "openid-connect", "attributes": { + "request.object.signature.alg": "any", + "request.object.encryption.alg": "any", + "client.introspection.response.allow.jwt.claim.enabled": "false", + "standard.token.exchange.enabled": "false", + "frontchannel.logout.session.required": "true", "post.logout.redirect.uris": "+", "oauth2.device.authorization.grant.enabled": "false", "backchannel.logout.revoke.offline.tokens": "false", "use.refresh.tokens": "true", + "realm_client": "false", "oidc.ciba.grant.enabled": "false", + "client.use.lightweight.access.token.enabled": "false", "backchannel.logout.session.required": "true", "client_credentials.use_refresh_token": "false", + "request.object.required": "not required", + "access.token.header.type.rfc9068": "false", "acr.loa.map": "{}", "require.pushed.authorization.requests": "false", "tls.client.certificate.bound.access.tokens": "false", "display.on.consent.screen": "false", + "request.object.encryption.enc": "any", "pkce.code.challenge.method": "S256", "token.response.type.bearer.lower-case": "false" }, @@ -419,16 +563,28 @@ "consentRequired": false, "config": { "full.path": "true", - "userinfo.token.claim": "true", - "multivalued": "true", "id.token.claim": "true", "access.token.claim": "true", - "claim.name": "groups" + "claim.name": "groups", + "userinfo.token.claim": "true", + "multivalued": "true" } } ], - "defaultClientScopes": ["web-origins", "acr", "profile", "roles", "email"], - "optionalClientScopes": ["address", "phone", "offline_access", "microprofile-jwt"] + "defaultClientScopes": [ + "web-origins", + "acr", + "profile", + "roles", + "basic", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] } ], "clientScopes": [ @@ -456,6 +612,45 @@ } ] }, + { + "id": "e5143dfa-a872-4b7e-80d2-7dd9f4f5ab68", + "name": "basic", + "description": "OpenID Connect scope for add all basic claims to the token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "53c0bcdd-61ed-49db-ab34-6a13394bab68", + "name": "auth_time", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "AUTH_TIME", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "auth_time", + "jsonType.label": "long" + } + }, + { + "id": "87b46444-d17f-418e-b980-d016e0961865", + "name": "sub", + "protocol": "openid-connect", + "protocolMapper": "oidc-sub-mapper", + "consentRequired": false, + "config": { + "introspection.token.claim": "true", + "access.token.claim": "true" + } + } + ] + }, { "id": "5b1d9553-1635-4ccf-9dc3-ffb88826322f", "name": "profile", @@ -463,8 +658,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" + "consent.screen.text": "${profileScopeConsentText}", + "display.on.consent.screen": "true" }, "protocolMappers": [ { @@ -486,12 +681,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "birthdate", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "birthdate", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -501,12 +696,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "middleName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "middle_name", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -516,12 +711,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "zoneinfo", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "zoneinfo", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -531,12 +726,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "website", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "website", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -546,12 +741,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "username", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "preferred_username", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -561,12 +756,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "picture", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "picture", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -576,12 +771,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "gender", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "gender", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -591,12 +786,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "updatedAt", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "updated_at", - "jsonType.label": "long" + "jsonType.label": "long", + "userinfo.token.claim": "true" } }, { @@ -606,12 +801,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "profile", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "profile", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -621,12 +816,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "locale", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "locale", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -636,12 +831,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "lastName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "family_name", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -651,12 +846,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "nickname", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "nickname", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -666,12 +861,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "firstName", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "given_name", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } } ] @@ -717,12 +912,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "username", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "upn", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } }, { @@ -750,8 +945,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" + "consent.screen.text": "${emailScopeConsentText}", + "display.on.consent.screen": "true" }, "protocolMappers": [ { @@ -761,12 +956,12 @@ "protocolMapper": "oidc-usermodel-property-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "emailVerified", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "email_verified", - "jsonType.label": "boolean" + "jsonType.label": "boolean", + "userinfo.token.claim": "true" } }, { @@ -776,12 +971,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "email", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "email", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } } ] @@ -803,8 +998,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" + "consent.screen.text": "${addressScopeConsentText}", + "display.on.consent.screen": "true" }, "protocolMappers": [ { @@ -834,8 +1029,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" + "consent.screen.text": "${rolesScopeConsentText}", + "display.on.consent.screen": "true" }, "protocolMappers": [ { @@ -876,6 +1071,66 @@ } ] }, + { + "id": "4d7ed3e9-8675-46ce-871f-b6f2c723c985", + "name": "service_account", + "description": "Specific scope for a client enabled for service accounts", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "153cfb83-7291-4ad7-a4dd-faa7b40c3d0b", + "name": "Client Host", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientHost", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientHost", + "jsonType.label": "String" + } + }, + { + "id": "563db9ad-5bce-4808-b628-07adc49c6bfd", + "name": "Client ID", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "client_id", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "client_id", + "jsonType.label": "String" + } + }, + { + "id": "824398d0-856e-4304-a7ff-2bd280204d53", + "name": "Client IP Address", + "protocol": "openid-connect", + "protocolMapper": "oidc-usersessionmodel-note-mapper", + "consentRequired": false, + "config": { + "user.session.note": "clientAddress", + "introspection.token.claim": "true", + "userinfo.token.claim": "true", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "clientAddress", + "jsonType.label": "String" + } + } + ] + }, { "id": "dff347a2-af32-47e3-a559-64efbaea9975", "name": "web-origins", @@ -883,8 +1138,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" + "consent.screen.text": "", + "display.on.consent.screen": "false" }, "protocolMappers": [ { @@ -904,8 +1159,8 @@ "protocol": "openid-connect", "attributes": { "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" + "consent.screen.text": "${phoneScopeConsentText}", + "display.on.consent.screen": "true" }, "protocolMappers": [ { @@ -915,12 +1170,12 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "phoneNumberVerified", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "phone_number_verified", - "jsonType.label": "boolean" + "jsonType.label": "boolean", + "userinfo.token.claim": "true" } }, { @@ -930,19 +1185,32 @@ "protocolMapper": "oidc-usermodel-attribute-mapper", "consentRequired": false, "config": { - "userinfo.token.claim": "true", "user.attribute": "phoneNumber", "id.token.claim": "true", "access.token.claim": "true", "claim.name": "phone_number", - "jsonType.label": "String" + "jsonType.label": "String", + "userinfo.token.claim": "true" } } ] } ], - "defaultDefaultClientScopes": ["role_list", "profile", "email", "roles", "web-origins", "acr"], - "defaultOptionalClientScopes": ["offline_access", "address", "phone", "microprofile-jwt"], + "defaultDefaultClientScopes": [ + "role_list", + "profile", + "email", + "roles", + "web-origins", + "acr", + "basic" + ], + "defaultOptionalClientScopes": [ + "offline_access", + "address", + "phone", + "microprofile-jwt" + ], "browserSecurityHeaders": { "contentSecurityPolicyReportOnly": "", "xContentTypeOptions": "nosniff", @@ -954,17 +1222,17 @@ "strictTransportSecurity": "max-age=31536000; includeSubDomains" }, "smtpServer": { + "password": "**********", "replyToDisplayName": "WunderGraph Cosmo", "starttls": "true", "auth": "true", - "envelopeFrom": "", - "ssl": "false", - "password": "**********", "port": "587", "host": "smtp.postmarkapp.com", "replyTo": "", "from": "system@wundergraph.com", "fromDisplayName": "WunderGraph Cosmo", + "envelopeFrom": "", + "ssl": "false", "user": "" }, "loginTheme": "cosmo", @@ -972,7 +1240,9 @@ "adminTheme": "", "emailTheme": "cosmo", "eventsEnabled": false, - "eventsListeners": ["jboss-logging"], + "eventsListeners": [ + "jboss-logging" + ], "enabledEventTypes": [], "adminEventsEnabled": false, "adminEventsDetailsEnabled": false, @@ -987,7 +1257,9 @@ "subType": "anonymous", "subComponents": {}, "config": { - "max-clients": ["200"] + "max-clients": [ + "200" + ] } }, { @@ -997,7 +1269,9 @@ "subType": "authenticated", "subComponents": {}, "config": { - "allow-default-scopes": ["true"] + "allow-default-scopes": [ + "true" + ] } }, { @@ -1007,7 +1281,9 @@ "subType": "anonymous", "subComponents": {}, "config": { - "allow-default-scopes": ["true"] + "allow-default-scopes": [ + "true" + ] } }, { @@ -1018,14 +1294,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ - "saml-user-property-mapper", - "oidc-address-mapper", "saml-role-list-mapper", - "oidc-usermodel-property-mapper", - "saml-user-attribute-mapper", + "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper" + "saml-user-attribute-mapper", + "oidc-address-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-usermodel-property-mapper" ] } }, @@ -1045,14 +1321,14 @@ "subComponents": {}, "config": { "allowed-protocol-mapper-types": [ + "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", - "saml-user-property-mapper", - "oidc-sha256-pairwise-sub-mapper", "oidc-address-mapper", "saml-role-list-mapper", - "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", - "oidc-usermodel-property-mapper" + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper" ] } }, @@ -1063,8 +1339,12 @@ "subType": "anonymous", "subComponents": {}, "config": { - "host-sending-registration-request-must-match": ["true"], - "client-uris-must-match": ["true"] + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] } }, { @@ -1076,6 +1356,18 @@ "config": {} } ], + "org.keycloak.userprofile.UserProfileProvider": [ + { + "id": "acbe748e-7593-420b-827c-b7856dd2cf71", + "providerId": "declarative-user-profile", + "subComponents": {}, + "config": { + "kc.user.profile.config": [ + "{\"attributes\":[{\"name\":\"username\",\"displayName\":\"${username}\",\"validations\":{\"length\":{\"min\":3,\"max\":255},\"username-prohibited-characters\":{},\"up-username-not-idn-homograph\":{}},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"email\",\"displayName\":\"${email}\",\"validations\":{\"email\":{},\"length\":{\"max\":255}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"firstName\",\"displayName\":\"${firstName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false},{\"name\":\"lastName\",\"displayName\":\"${lastName}\",\"validations\":{\"length\":{\"max\":255},\"person-name-prohibited-characters\":{}},\"required\":{\"roles\":[\"user\"]},\"permissions\":{\"view\":[\"admin\",\"user\"],\"edit\":[\"admin\",\"user\"]},\"multivalued\":false}],\"groups\":[{\"name\":\"user-metadata\",\"displayHeader\":\"User metadata\",\"displayDescription\":\"Attributes, which refer to user metadata\"}],\"unmanagedAttributePolicy\":\"ENABLED\"}" + ] + } + } + ], "org.keycloak.keys.KeyProvider": [ { "id": "44d60df3-86f6-4943-9eb3-5a2b661d08a6", @@ -1083,7 +1375,9 @@ "providerId": "aes-generated", "subComponents": {}, "config": { - "priority": ["100"] + "priority": [ + "100" + ] } }, { @@ -1092,8 +1386,12 @@ "providerId": "hmac-generated", "subComponents": {}, "config": { - "priority": ["100"], - "algorithm": ["HS256"] + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] } }, { @@ -1102,7 +1400,9 @@ "providerId": "rsa-generated", "subComponents": {}, "config": { - "priority": ["100"] + "priority": [ + "100" + ] } }, { @@ -1111,8 +1411,26 @@ "providerId": "rsa-enc-generated", "subComponents": {}, "config": { - "priority": ["100"], - "algorithm": ["RSA-OAEP"] + "priority": [ + "100" + ], + "algorithm": [ + "RSA-OAEP" + ] + } + }, + { + "id": "30fd4302-d870-4d9e-9e20-994d7568ce45", + "name": "hmac-generated-hs512", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "priority": [ + "100" + ], + "algorithm": [ + "HS512" + ] } } ] @@ -1371,6 +1689,108 @@ } ] }, + { + "id": "247d2578-faee-4b43-a37c-b222e96d9f5b", + "alias": "browser alt", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-spnego", + "authenticatorFlow": false, + "requirement": "DISABLED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "identity-provider-redirector", + "authenticatorFlow": false, + "requirement": "ALTERNATIVE", + "priority": 25, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "ALTERNATIVE", + "priority": 30, + "autheticatorFlow": true, + "flowAlias": "browser alt forms", + "userSetupAllowed": false + } + ] + }, + { + "id": "c22f7df8-9f25-482c-ae9c-7260b87e054e", + "alias": "browser alt Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-otp-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + } + ] + }, + { + "id": "8de60f3b-1624-4465-9c3c-599c9f03d23e", + "alias": "browser alt forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": false, + "authenticationExecutions": [ + { + "authenticator": "sso-cookie-authenticator", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 10, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticator": "auth-username-password-form", + "authenticatorFlow": false, + "requirement": "REQUIRED", + "priority": 20, + "autheticatorFlow": false, + "userSetupAllowed": false + }, + { + "authenticatorFlow": true, + "requirement": "CONDITIONAL", + "priority": 21, + "autheticatorFlow": true, + "flowAlias": "browser alt Browser - Conditional OTP", + "userSetupAllowed": false + } + ] + }, { "id": "b7dc71a8-d79e-4edd-a244-0d99ac170522", "alias": "clients", @@ -1553,14 +1973,6 @@ "autheticatorFlow": false, "userSetupAllowed": false }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "autheticatorFlow": false, - "userSetupAllowed": false - }, { "authenticator": "registration-password-action", "authenticatorFlow": false, @@ -1729,6 +2141,24 @@ "priority": 80, "config": {} }, + { + "alias": "delete_credential", + "name": "Delete Credential", + "providerId": "delete_credential", + "enabled": true, + "defaultAction": false, + "priority": 100, + "config": {} + }, + { + "alias": "idp_link", + "name": "Linking Identity Provider", + "providerId": "idp_link", + "enabled": true, + "defaultAction": false, + "priority": 110, + "config": {} + }, { "alias": "update_user_locale", "name": "Update User Locale", @@ -1745,6 +2175,7 @@ "resetCredentialsFlow": "reset credentials", "clientAuthenticationFlow": "clients", "dockerAuthenticationFlow": "docker auth", + "firstBrokerLoginFlow": "first broker login", "attributes": { "cibaBackchannelTokenDeliveryMode": "poll", "cibaAuthRequestedUserHint": "login_hint", @@ -1765,12 +2196,15 @@ "frontendUrl": "http://localhost:8080", "shortVerificationUri": "" }, - "keycloakVersion": "22.0.3", + "keycloakVersion": "26.2.5", "userManagedAccessAllowed": false, + "organizationsEnabled": false, + "verifiableCredentialsEnabled": false, + "adminPermissionsEnabled": false, "clientProfiles": { "profiles": [] }, "clientPolicies": { "policies": [] } -} +} \ No newline at end of file diff --git a/keycloak/Dockerfile b/keycloak/Dockerfile index cfd2812adc..935eec6e21 100644 --- a/keycloak/Dockerfile +++ b/keycloak/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=${BUILDPLATFORM} timbru31/java-node:11-jdk-18 +FROM --platform=${BUILDPLATFORM} timbru31/java-node:17-jdk-18 WORKDIR /app @@ -12,7 +12,7 @@ COPY ./theme . RUN ./build.sh -FROM --platform=${BUILDPLATFORM} bitnamilegacy/keycloak:25.0.2 +FROM --platform=${BUILDPLATFORM} bitnamilegacy/keycloak:26.2.5 COPY --from=0 /app/target/*.jar /opt/bitnami/keycloak/providers/ diff --git a/keycloak/theme/pom.xml b/keycloak/theme/pom.xml index ba1b59b73a..272702015e 100644 --- a/keycloak/theme/pom.xml +++ b/keycloak/theme/pom.xml @@ -5,9 +5,43 @@ com.cosmo cosmo - 1.0 + 1.10 cosmo jar + + 11 + 11 + UTF-8 + 26.2.5 + + + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + jakarta.ws.rs + jakarta.ws.rs-api + 3.1.0 + provided + + + diff --git a/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticator.java b/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticator.java new file mode 100644 index 0000000000..34be23a790 --- /dev/null +++ b/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticator.java @@ -0,0 +1,131 @@ +package com.wundergraph.authentication; + +import jakarta.ws.rs.core.*; +import org.keycloak.authentication.AuthenticationFlowContext; +import org.keycloak.authentication.Authenticator; +import org.keycloak.models.*; +import org.keycloak.protocol.LoginProtocol; +import org.keycloak.sessions.AuthenticationSessionModel; + +import java.util.Map; + +public class SSOCookieAuthenticator implements Authenticator { + @Override + public void authenticate(AuthenticationFlowContext authenticationFlowContext) { + String ssoCookieName = getSSOCookieName(authenticationFlowContext); + + // Retrieve the configured SSO cookie name + Map cookies = authenticationFlowContext.getHttpRequest().getHttpHeaders().getCookies(); + Cookie ssoCookie = cookies.getOrDefault(ssoCookieName, null); + if (ssoCookie == null) { + // The SSO cookie was not found + authenticationFlowContext.success(); + return; + } + + // Ensure that the SSO cookie value is not empty and set the hint note + String ssoCookieValue = ssoCookie.getValue(); + if (ssoCookieValue == null || ssoCookieValue.trim().isEmpty()) { + // The SSO cookie doesn't exist or the value is empty + authenticationFlowContext.success(); + return; + } + + ssoCookieValue = ssoCookieValue.trim(); + + // Make sure that value of the SSO cookie is a registered and enabled IDP + KeycloakSession session = authenticationFlowContext.getSession(); + RealmModel realm = authenticationFlowContext.getRealm(); + + IdentityProviderStorageProvider storage = session.getProvider(IdentityProviderStorageProvider.class); + IdentityProviderModel idpModel = storage.getByAlias(ssoCookieValue); + + if (idpModel != null && idpModel.isEnabled()) { + // Create the login URL for it and pass it to the frontend + String ssoLoginUrl = composeLoginUrl(realm, idpModel, authenticationFlowContext); + authenticationFlowContext.form().setAttribute("ssoLoginUrl", ssoLoginUrl); + } + + authenticationFlowContext.success(); + } + + @Override + public void action(AuthenticationFlowContext authenticationFlowContext) { + // No-op + } + + @Override + public boolean requiresUser() { + return false; + } + + @Override + public boolean configuredFor(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { + return true; + } + + @Override + public void setRequiredActions(KeycloakSession keycloakSession, RealmModel realmModel, UserModel userModel) { + // No-op + } + + @Override + public void close() { + // No-op + } + + private static String getSSOCookieName(AuthenticationFlowContext authenticationFlowContext) { + AuthenticatorConfigModel config = authenticationFlowContext.getAuthenticatorConfig(); + if (config == null) { + return SSOCookieAuthenticatorFactory.DEFAULT_COOKIE_NAME; + } + + // Retrieve the SSO cookie name configuration + String ssoCookieName = config.getConfig().getOrDefault( + SSOCookieAuthenticatorFactory.SSO_COOKIE_CONFIG_NAME, + SSOCookieAuthenticatorFactory.DEFAULT_COOKIE_NAME + ); + + if (ssoCookieName == null || ssoCookieName.isEmpty()) { + // The cookie name has not been configured + return SSOCookieAuthenticatorFactory.DEFAULT_COOKIE_NAME; + } + + return ssoCookieName.trim(); + } + + private static String composeLoginUrl( + RealmModel realm, + IdentityProviderModel idpModel, + AuthenticationFlowContext authenticationFlowContext) + { + KeycloakSession session = authenticationFlowContext.getSession(); + AuthenticationSessionModel authSession = authenticationFlowContext.getAuthenticationSession(); + UriInfo uriInfo = authenticationFlowContext.getUriInfo(); + + // Adapted from + // https://github.com/keycloak/keycloak/blob/10947d002fa9c70c26e7b5a266f71f83d5c2688b/services/src/main/java/org/keycloak/authentication/AuthenticationProcessor.java#L308 + LoginProtocol protocol = session.getProvider(LoginProtocol.class, authSession.getProtocol()); + String clientData = protocol.getClientData(authSession).encode(); + + // Build the URL + UriBuilder uriBuilder = uriInfo.getBaseUriBuilder() + .path("realms") + .path(realm.getName()) + .path("broker") + .path(idpModel.getAlias()) + .path("login"); + + uriBuilder.queryParam(Constants.CLIENT_ID, authSession.getClient().getClientId()); + uriBuilder.queryParam(Constants.TAB_ID, authSession.getTabId()); + uriBuilder.queryParam(Constants.CLIENT_DATA, clientData); + uriBuilder.queryParam("session_code", authenticationFlowContext.generateAccessCode()); + + String loginHint = authSession.getClientNote("login_hint"); + if (loginHint != null && !loginHint.isEmpty()) { + uriBuilder.queryParam("login_hint", loginHint); + } + + return uriBuilder.build().toString(); + } +} diff --git a/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticatorFactory.java b/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticatorFactory.java new file mode 100644 index 0000000000..37b3a52925 --- /dev/null +++ b/keycloak/theme/src/main/java/com/wundergraph/authentication/SSOCookieAuthenticatorFactory.java @@ -0,0 +1,92 @@ +package com.wundergraph.authentication; + +import org.keycloak.Config; +import org.keycloak.authentication.Authenticator; +import org.keycloak.authentication.AuthenticatorFactory; +import org.keycloak.models.AuthenticationExecutionModel; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; +import org.keycloak.provider.ProviderConfigProperty; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SSOCookieAuthenticatorFactory implements AuthenticatorFactory { + public static final String PROVIDER_ID = "sso-cookie-authenticator"; + public static final String SSO_COOKIE_CONFIG_NAME = "sso-cookie-name"; + public static final String DEFAULT_COOKIE_NAME = "cosmo_idp_hint"; + + private static final List configProperties; + + static { + ProviderConfigProperty prop = new ProviderConfigProperty(); + prop.setName(SSO_COOKIE_CONFIG_NAME); + prop.setLabel("Cookie Name"); + prop.setHelpText("The name of the SSO Cookie"); + prop.setDefaultValue(DEFAULT_COOKIE_NAME); + prop.setRequired(true); + prop.setType(ProviderConfigProperty.STRING_TYPE); + + configProperties = List.of(prop); + } + + @Override + public String getId() { + return PROVIDER_ID; + } + + @Override + public String getDisplayType() { + return "SSO Cookie Check"; + } + + @Override + public Authenticator create(KeycloakSession session) { + return new SSOCookieAuthenticator(); + } + + @Override + public void init(Config.Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getReferenceCategory() { + return ""; + } + + @Override + public boolean isConfigurable() { + return true; + } + + @Override + public AuthenticationExecutionModel.Requirement[] getRequirementChoices() { + return new AuthenticationExecutionModel.Requirement[]{ + AuthenticationExecutionModel.Requirement.REQUIRED, + }; + } + + @Override + public List getConfigProperties() { + return configProperties; + } + + @Override + public boolean isUserSetupAllowed() { + return false; + } + + @Override + public String getHelpText() { + return "Retrieve the SSO cookie and construct an additional SSO login link based on it."; + } +} diff --git a/keycloak/theme/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory b/keycloak/theme/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory new file mode 100644 index 0000000000..68c942e633 --- /dev/null +++ b/keycloak/theme/src/main/resources/META-INF/services/org.keycloak.authentication.AuthenticatorFactory @@ -0,0 +1 @@ +com.wundergraph.authentication.SSOCookieAuthenticatorFactory \ No newline at end of file diff --git a/keycloak/theme/src/main/resources/theme/cosmo/login/login.ftl b/keycloak/theme/src/main/resources/theme/cosmo/login/login.ftl index 35051bb241..3f3d65dead 100644 --- a/keycloak/theme/src/main/resources/theme/cosmo/login/login.ftl +++ b/keycloak/theme/src/main/resources/theme/cosmo/login/login.ftl @@ -4,10 +4,10 @@ ${msg("loginAccountTitle")} <#elseif section = "form">
- <#if realm.password && social.providers??> + <#if realm.password && client.clientId != "studio" && social.providers??>
diff --git a/keycloak/theme/src/main/resources/theme/cosmo/login/messages/messages_en.properties b/keycloak/theme/src/main/resources/theme/cosmo/login/messages/messages_en.properties index 165feca43d..286709d065 100755 --- a/keycloak/theme/src/main/resources/theme/cosmo/login/messages/messages_en.properties +++ b/keycloak/theme/src/main/resources/theme/cosmo/login/messages/messages_en.properties @@ -3,3 +3,5 @@ termsText=

Thank you for your interest in WunderGraph Cosmo! We're happy you'r loginAccountTitle= Sign in emailInstruction=Enter your email address and we will send you instructions on how to create a new password. oauthGrantRequest=Do you want to grant access to cosmo-cli? + +signInWithSSO=Sign in with SSO \ No newline at end of file diff --git a/keycloak/theme/src/main/resources/theme/cosmo/login/theme.properties b/keycloak/theme/src/main/resources/theme/cosmo/login/theme.properties index f43ef74f01..b415ecda3f 100644 --- a/keycloak/theme/src/main/resources/theme/cosmo/login/theme.properties +++ b/keycloak/theme/src/main/resources/theme/cosmo/login/theme.properties @@ -2,7 +2,7 @@ parent=base import=common/keycloak styles=dist/scss/login.css -stylesCommon=node_modules/@patternfly/patternfly/patternfly.min.css node_modules/patternfly/dist/css/patternfly.min.css node_modules/patternfly/dist/css/patternfly-additions.min.css lib/pficon/pficon.css +stylesCommon=vendor/patternfly-v4/patternfly.min.css vendor/patternfly-v3/css/patternfly.min.css vendor/patternfly-v3/css/patternfly-additions.min.css lib/pficon/pficon.css meta=viewport==width=device-width,initial-scale=1 @@ -38,8 +38,8 @@ kcFormSocialAccountListButtonClass=pf-v5-c-button pf-v5-m-control pf-v5-m-block kcFormSocialAccountGridItem=pf-v5-l-grid__item kcFormSocialAccountNameClass=kc-social-provider-name -kcFormSocialAccountLinkClass=pf-v5-c-login__main-footer-links-item-link -kcFormSocialAccountSectionClass=kc-social-section kc-social-gray grid grid-flow-col auto-cols-fr gap-x-2 +kcFormSocialAccountLinkClass= +kcFormSocialAccountSectionClass=mb-4 space-y-3 kcFormHeaderClass=login-pf-header kcFeedbackErrorIcon=fa fa-fw fa-exclamation-circle diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 699eacd15d..eabdb91317 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -488,8 +488,8 @@ importers: specifier: ^10.1.2 version: 10.1.2(graphql@16.9.0(patch_hash=hafdlc54qtxpqvetpefk646rly)) '@keycloak/keycloak-admin-client': - specifier: ^25.0.2 - version: 25.0.6 + specifier: 26.2.5 + version: 26.2.5 '@octokit/webhooks-types': specifier: ^7.6.1 version: 7.6.1 @@ -4085,8 +4085,8 @@ packages: '@jspm/core@2.0.1': resolution: {integrity: sha512-Lg3PnLp0QXpxwLIAuuJboLeRaIhrgJjeuh797QADg3xz8wGLugQOS5DpsE8A6i6Adgzf+bacllkKZG3J0tGfDw==} - '@keycloak/keycloak-admin-client@25.0.6': - resolution: {integrity: sha512-rUvo6L0aT9+y/R2wFhDKb+lRD5rwj36XwnIGIbmC2HeQVfgroYB5u/79yc/Mo0inBFNIG8VuiwjRzU878fRE0Q==} + '@keycloak/keycloak-admin-client@26.2.5': + resolution: {integrity: sha512-glMBjQE0M8KOe9uZtdIl7kvHeBjfegZKxK8SzAZxekWvwsZ3Kt+XNd1kKW7NUxRdrs9EGtJLNUWX8vfaR+0Snw==} engines: {node: '>=18'} '@lerna-lite/cli@4.1.1': @@ -17871,7 +17871,7 @@ snapshots: '@jspm/core@2.0.1': {} - '@keycloak/keycloak-admin-client@25.0.6': + '@keycloak/keycloak-admin-client@26.2.5': dependencies: camelize-ts: 3.0.0 url-join: 5.0.0