From 81d0ee096e24ce93ef8a47a3a88114a96ea5b869 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 17:06:11 +0200 Subject: [PATCH 01/61] api oidc authentication mechanism --- docker-compose-dev.yml | 39 +- keycloak/oauth2-proxy-realm.json | 2071 +++++++++++++++++ keycloak/oauth2-proxy-users-0.json | 38 + .../iq/dataverse/api/AbstractApiBean.java | 17 +- .../dataverse/api/OpenIDAuthentication.java | 34 + .../iq/dataverse/api/OpenIDCallback.java | 48 + .../iq/dataverse/api/OpenIDConfigBean.java | 25 + .../iq/dataverse/settings/JvmSettings.java | 7 + 8 files changed, 2258 insertions(+), 21 deletions(-) create mode 100644 keycloak/oauth2-proxy-realm.json create mode 100644 keycloak/oauth2-proxy-users-0.json create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 402a95c0e16..2d7744fab58 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -16,13 +16,12 @@ services: ENABLE_RELOAD: "1" SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" - DATAVERSE_AUTH_OIDC_ENABLED: "1" - DATAVERSE_AUTH_OIDC_CLIENT_ID: test - DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 - DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test + DATAVERSE_AUTH_API_OIDC_CLIENT_ID: oauth2-proxy + DATAVERSE_AUTH_API_OIDC_CLIENT_SECRET: 72341b6d-7065-4518-a0e4-50ee15025608 + DATAVERSE_AUTH_API_OIDC_PROVIDER_URI: http://172.17.0.1:9080/realms/oauth2-proxy + DATAVERSE_AUTH_API_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters" # These two oai settings are here to get HarvestingServerIT to pass dataverse_oai_server_maxidentifiers: "2" @@ -164,24 +163,24 @@ services: tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 - dev_keycloak: - container_name: "dev_keycloak" - image: 'quay.io/keycloak/keycloak:21.0' + keycloak: + container_name: keycloak + image: keycloak/keycloak:25.0 hostname: keycloak + command: + - 'start-dev' + - '--http-port=9080' + - '--import-realm' + volumes: + - ./keycloak:/opt/keycloak/data/import environment: - - KEYCLOAK_ADMIN=kcadmin - - KEYCLOAK_ADMIN_PASSWORD=kcpassword - - KEYCLOAK_LOGLEVEL=DEBUG - - KC_HOSTNAME_STRICT=false - networks: - dataverse: - aliases: - - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) - command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + KC_HTTP_PORT: 9080 + KEYCLOAK_ADMIN: admin@kuleuven.be + KEYCLOAK_ADMIN_PASSWORD: password ports: - - "8090:8090" - volumes: - - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' + - 9080:9080 + networks: + - dataverse # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! diff --git a/keycloak/oauth2-proxy-realm.json b/keycloak/oauth2-proxy-realm.json new file mode 100644 index 00000000000..ac69b2375fd --- /dev/null +++ b/keycloak/oauth2-proxy-realm.json @@ -0,0 +1,2071 @@ +{ + "id": "oauth2-proxy", + "realm": "oauth2-proxy", + "displayName": "Keycloak", + "displayNameHtml": "
Keycloak
", + "notBefore": 0, + "revokeRefreshToken": false, + "refreshTokenMaxReuse": 0, + "accessTokenLifespan": 60, + "accessTokenLifespanForImplicitFlow": 900, + "ssoSessionIdleTimeout": 1800, + "ssoSessionMaxLifespan": 36000, + "ssoSessionIdleTimeoutRememberMe": 0, + "ssoSessionMaxLifespanRememberMe": 0, + "offlineSessionIdleTimeout": 2592000, + "offlineSessionMaxLifespanEnabled": false, + "offlineSessionMaxLifespan": 5184000, + "clientSessionIdleTimeout": 0, + "clientSessionMaxLifespan": 0, + "accessCodeLifespan": 60, + "accessCodeLifespanUserAction": 300, + "accessCodeLifespanLogin": 1800, + "actionTokenGeneratedByAdminLifespan": 43200, + "actionTokenGeneratedByUserLifespan": 300, + "enabled": true, + "sslRequired": "external", + "registrationAllowed": false, + "registrationEmailAsUsername": false, + "rememberMe": false, + "verifyEmail": false, + "loginWithEmailAllowed": true, + "duplicateEmailsAllowed": false, + "resetPasswordAllowed": false, + "editUsernameAllowed": false, + "bruteForceProtected": false, + "permanentLockout": false, + "maxFailureWaitSeconds": 900, + "minimumQuickLoginWaitSeconds": 60, + "waitIncrementSeconds": 60, + "quickLoginCheckMilliSeconds": 1000, + "maxDeltaTimeSeconds": 43200, + "failureFactor": 30, + "roles": { + "realm": [ + { + "id": "32626c92-4327-40f1-b318-76a6b5c7eee5", + "name": "offline_access", + "description": "${role_offline-access}", + "composite": false, + "clientRole": false, + "containerId": "oauth2-proxy", + "attributes": {} + }, + { + "id": "e36da570-7ae0-4323-8b39-73eb92ce722f", + "name": "admin", + "description": "${role_admin}", + "composite": true, + "composites": { + "realm": [ + "create-realm" + ], + "client": { + "oauth2-proxy-realm": [ + "query-groups", + "create-client", + "query-realms", + "view-authorization", + "view-realm", + "manage-clients", + "query-users", + "manage-realm", + "view-events", + "manage-events", + "view-identity-providers", + "view-users", + "manage-identity-providers", + "manage-authorization", + "manage-users", + "view-clients", + "query-clients", + "impersonation" + ] + } + }, + "clientRole": false, + "containerId": "oauth2-proxy", + "attributes": {} + }, + { + "id": "71aca46c-6fcf-4456-ba87-6374e70108a2", + "name": "uma_authorization", + "description": "${role_uma_authorization}", + "composite": false, + "clientRole": false, + "containerId": "oauth2-proxy", + "attributes": {} + }, + { + "id": "6ca3fee8-1a3f-4068-a311-6e81223a884b", + "name": "create-realm", + "description": "${role_create-realm}", + "composite": false, + "clientRole": false, + "containerId": "oauth2-proxy", + "attributes": {} + } + ], + "client": { + "oauth2-proxy": [], + "security-admin-console": [], + "admin-cli": [], + "account-console": [], + "broker": [ + { + "id": "2cc5e40c-0a28-4c09-85eb-20cd47ac1351", + "name": "read-token", + "description": "${role_read-token}", + "composite": false, + "clientRole": true, + "containerId": "380985f1-61c7-4940-93ae-7a09458071ca", + "attributes": {} + } + ], + "oauth2-proxy-realm": [ + { + "id": "a8271c2c-6437-4ca5-ae83-49ea5fe1318d", + "name": "query-groups", + "description": "${role_query-groups}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "5a7cb1ae-7dac-486b-bf7b-4d7fbc5adb31", + "name": "create-client", + "description": "${role_create-client}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "a9e6a2fa-c31b-4959-bf8a-a46fcc9c65ec", + "name": "view-authorization", + "description": "${role_view-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "1cef34e3-569a-4d2b-ba5c-aafe5c7ab423", + "name": "query-realms", + "description": "${role_query-realms}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "efc46075-30cd-4600-aa92-2ae4a171d0c2", + "name": "view-realm", + "description": "${role_view-realm}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "9ffacaf0-afc6-49e9-8708-ef35ac40f3f8", + "name": "manage-clients", + "description": "${role_manage-clients}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "90662091-b3bc-4ae4-83c9-a4f53e7e9eeb", + "name": "query-users", + "description": "${role_query-users}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "9a5fbc9d-6fae-4155-86f6-72fd399aa126", + "name": "manage-realm", + "description": "${role_manage-realm}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "03f46127-9436-477d-8c7f-58569f45237c", + "name": "view-events", + "description": "${role_view-events}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "f10eaea2-90ab-4310-9d5f-8d986564d061", + "name": "view-identity-providers", + "description": "${role_view-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "2403e038-2cf7-4b06-b5cb-33a417a00d8d", + "name": "manage-events", + "description": "${role_manage-events}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "677d057b-66f8-4163-9948-95fdbd06dfdc", + "name": "view-users", + "description": "${role_view-users}", + "composite": true, + "composites": { + "client": { + "oauth2-proxy-realm": [ + "query-groups", + "query-users" + ] + } + }, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "dc140fa6-bf2c-49f2-b8c9-fc34ef8a2c63", + "name": "manage-identity-providers", + "description": "${role_manage-identity-providers}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "155bf234-4895-4855-95c2-a460518f57e8", + "name": "manage-authorization", + "description": "${role_manage-authorization}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "5441ec71-3eac-4696-9e68-0de54fbdde98", + "name": "manage-users", + "description": "${role_manage-users}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "2db0f052-cb91-4170-81fd-107756b162f7", + "name": "view-clients", + "description": "${role_view-clients}", + "composite": true, + "composites": { + "client": { + "oauth2-proxy-realm": [ + "query-clients" + ] + } + }, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "e1d7f235-8bf2-40b8-be49-49aca70a5088", + "name": "query-clients", + "description": "${role_query-clients}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + }, + { + "id": "e743f66a-2f56-4b97-b34b-33f06ff1e739", + "name": "impersonation", + "description": "${role_impersonation}", + "composite": false, + "clientRole": true, + "containerId": "7174c175-1887-4e57-b95b-969fe040deff", + "attributes": {} + } + ], + "account": [ + { + "id": "64d8f532-839e-4386-b2eb-fe8848b0a9de", + "name": "manage-consent", + "description": "${role_manage-consent}", + "composite": true, + "composites": { + "client": { + "account": [ + "view-consent" + ] + } + }, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + }, + { + "id": "3ec22748-960f-4f96-a43e-50f54a02dc23", + "name": "view-profile", + "description": "${role_view-profile}", + "composite": false, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + }, + { + "id": "177d18e4-46b0-4ea3-8b70-327486ce5bb2", + "name": "view-applications", + "description": "${role_view-applications}", + "composite": false, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + }, + { + "id": "703643d6-0542-4e27-9737-7c442925c18c", + "name": "manage-account-links", + "description": "${role_manage-account-links}", + "composite": false, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + }, + { + "id": "c64f9f66-d762-4337-8833-cf31c316e8a7", + "name": "view-consent", + "description": "${role_view-consent}", + "composite": false, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + }, + { + "id": "611f568b-0fdd-4d2e-ba34-03136cd486c4", + "name": "manage-account", + "description": "${role_manage-account}", + "composite": true, + "composites": { + "client": { + "account": [ + "manage-account-links" + ] + } + }, + "clientRole": true, + "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", + "attributes": {} + } + ] + } + }, + "groups": [], + "defaultRoles": [ + "offline_access", + "uma_authorization" + ], + "requiredCredentials": [ + "password" + ], + "otpPolicyType": "totp", + "otpPolicyAlgorithm": "HmacSHA1", + "otpPolicyInitialCounter": 0, + "otpPolicyDigits": 6, + "otpPolicyLookAheadWindow": 1, + "otpPolicyPeriod": 30, + "otpSupportedApplications": [ + "FreeOTP", + "Google Authenticator" + ], + "webAuthnPolicyRpEntityName": "keycloak", + "webAuthnPolicySignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyRpId": "", + "webAuthnPolicyAttestationConveyancePreference": "not specified", + "webAuthnPolicyAuthenticatorAttachment": "not specified", + "webAuthnPolicyRequireResidentKey": "not specified", + "webAuthnPolicyUserVerificationRequirement": "not specified", + "webAuthnPolicyCreateTimeout": 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyAcceptableAaguids": [], + "webAuthnPolicyPasswordlessRpEntityName": "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms": [ + "ES256" + ], + "webAuthnPolicyPasswordlessRpId": "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", + "webAuthnPolicyPasswordlessCreateTimeout": 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, + "webAuthnPolicyPasswordlessAcceptableAaguids": [], + "scopeMappings": [ + { + "clientScope": "offline_access", + "roles": [ + "offline_access" + ] + } + ], + "clientScopeMappings": { + "account": [ + { + "client": "account-console", + "roles": [ + "manage-account" + ] + } + ] + }, + "clients": [ + { + "id": "a367038f-fe01-4459-9f91-7ad0cf498533", + "clientId": "account", + "name": "${client_account}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/oauth2-proxy/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "0896a464-da81-4454-bee9-b56bdbad9e7f", + "defaultRoles": [ + "view-profile", + "manage-account" + ], + "redirectUris": [ + "/realms/oauth2-proxy/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "72f75604-1e21-407c-b967-790aafd11534", + "clientId": "account-console", + "name": "${client_account-console}", + "rootUrl": "${authBaseUrl}", + "baseUrl": "/realms/oauth2-proxy/account/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "91f85142-ee18-4e30-9949-e5acb701bdee", + "redirectUris": [ + "/realms/oauth2-proxy/account/*" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "2772c101-0dba-49b7-9627-5aaddc666939", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "b13fd0de-3be0-4a08-bc5d-d1de34421b1a", + "clientId": "admin-cli", + "name": "${client_admin-cli}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "4640af2e-b4a6-44eb-85ec-6278a62a4f01", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": false, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "380985f1-61c7-4940-93ae-7a09458071ca", + "clientId": "broker", + "name": "${client_broker}", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "65d2ba2b-bcae-49ff-9f56-77c818f55930", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "7174c175-1887-4e57-b95b-969fe040deff", + "clientId": "oauth2-proxy-realm", + "name": "oauth2-proxy Realm", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "40f73851-a94c-4091-90de-aeee8ca1acf8", + "redirectUris": [], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": true, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "attributes": {}, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": 0, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "0493c7c6-6e20-49ea-9acb-627c0b52d400", + "clientId": "oauth2-proxy", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "72341b6d-7065-4518-a0e4-50ee15025608", + "redirectUris": [ + "http://localhost:8080/api/v1/callback/token" + ], + "webOrigins": [], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": true, + "serviceAccountsEnabled": false, + "publicClient": false, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "saml.assertion.signature": "false", + "saml.force.post.binding": "false", + "saml.multivalued.roles": "false", + "saml.encrypt": "false", + "saml.server.signature": "false", + "saml.server.signature.keyinfo.ext": "false", + "exclude.session.state.from.auth.response": "false", + "saml_force_name_id_format": "false", + "saml.client.signature": "false", + "tls.client.certificate.bound.access.tokens": "false", + "saml.authnstatement": "false", + "display.on.consent.screen": "false", + "saml.onetimeuse.condition": "false" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": true, + "nodeReRegistrationTimeout": -1, + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + }, + { + "id": "2a3ad1fd-a30d-4b72-89c4-bed12f178338", + "clientId": "security-admin-console", + "name": "${client_security-admin-console}", + "rootUrl": "${authAdminUrl}", + "baseUrl": "/admin/oauth2-proxy/console/", + "surrogateAuthRequired": false, + "enabled": true, + "alwaysDisplayInConsole": false, + "clientAuthenticatorType": "client-secret", + "secret": "b234b7aa-8417-410f-b3fd-c57434d3aa4a", + "redirectUris": [ + "/admin/oauth2-proxy/console/*" + ], + "webOrigins": [ + "+" + ], + "notBefore": 0, + "bearerOnly": false, + "consentRequired": false, + "standardFlowEnabled": true, + "implicitFlowEnabled": false, + "directAccessGrantsEnabled": false, + "serviceAccountsEnabled": false, + "publicClient": true, + "frontchannelLogout": false, + "protocol": "openid-connect", + "attributes": { + "pkce.code.challenge.method": "S256" + }, + "authenticationFlowBindingOverrides": {}, + "fullScopeAllowed": false, + "nodeReRegistrationTimeout": 0, + "protocolMappers": [ + { + "id": "5885b0d3-a917-4b52-8380-f37d0754a2ef", + "name": "locale", + "protocol": "openid-connect", + "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" + } + } + ], + "defaultClientScopes": [ + "web-origins", + "role_list", + "roles", + "profile", + "email" + ], + "optionalClientScopes": [ + "address", + "phone", + "offline_access", + "microprofile-jwt" + ] + } + ], + "clientScopes": [ + { + "id": "47ea3b67-4f0c-4c7e-8ac6-a33a3d655894", + "name": "address", + "description": "OpenID Connect built-in scope: address", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${addressScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "4be0ca19-0ec7-4cc1-b263-845ea539ff12", + "name": "address", + "protocol": "openid-connect", + "protocolMapper": "oidc-address-mapper", + "consentRequired": false, + "config": { + "user.attribute.formatted": "formatted", + "user.attribute.country": "country", + "user.attribute.postal_code": "postal_code", + "userinfo.token.claim": "true", + "user.attribute.street": "street", + "id.token.claim": "true", + "user.attribute.region": "region", + "access.token.claim": "true", + "user.attribute.locality": "locality" + } + } + ] + }, + { + "id": "aba72e57-540f-4825-95b7-2d143be028cc", + "name": "email", + "description": "OpenID Connect built-in scope: email", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${emailScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "7fe82724-5748-4b6d-9708-a028f5d3b970", + "name": "email verified", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "e42f334e-cfae-44a0-905d-c3ef215feaae", + "name": "email", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-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" + } + } + ] + }, + { + "id": "ec765598-bd71-4318-86c3-b3f81a41c99e", + "name": "microprofile-jwt", + "description": "Microprofile - JWT built-in scope", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "false" + }, + "protocolMappers": [ + { + "id": "90694036-4014-4672-a2c8-c68319e9308a", + "name": "upn", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-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" + } + }, + { + "id": "f7b0fcc0-6139-4158-ac45-34fd9a58a5ef", + "name": "groups", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "multivalued": "true", + "user.attribute": "foo", + "id.token.claim": "true", + "access.token.claim": "true", + "claim.name": "groups", + "jsonType.label": "String" + } + } + ] + }, + { + "id": "8a09267b-3634-4a9c-baab-6f2fb4137347", + "name": "offline_access", + "description": "OpenID Connect built-in scope: offline_access", + "protocol": "openid-connect", + "attributes": { + "consent.screen.text": "${offlineAccessScopeConsentText}", + "display.on.consent.screen": "true" + } + }, + { + "id": "3a48c5dd-33a8-4be0-9d2e-30fd7f98363a", + "name": "phone", + "description": "OpenID Connect built-in scope: phone", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${phoneScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "5427d1b4-ba79-412a-b23c-da640a98980c", + "name": "phone number", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "31d4a53f-6503-40e8-bd9d-79a7c46c4fbe", + "name": "phone number verified", + "protocol": "openid-connect", + "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" + } + } + ] + }, + { + "id": "5921a9e9-7fec-4471-95e3-dd96eebdec58", + "name": "profile", + "description": "OpenID Connect built-in scope: profile", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "true", + "display.on.consent.screen": "true", + "consent.screen.text": "${profileScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "4fa92092-ee0d-4dc7-a63b-1e3b02d35ebb", + "name": "zoneinfo", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "1a5cc2e2-c983-4150-8583-23a7f5c826bf", + "name": "family name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-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" + } + }, + { + "id": "67931f77-722a-492d-b581-a953e26b7d44", + "name": "full name", + "protocol": "openid-connect", + "protocolMapper": "oidc-full-name-mapper", + "consentRequired": false, + "config": { + "id.token.claim": "true", + "access.token.claim": "true", + "userinfo.token.claim": "true" + } + }, + { + "id": "10f6ac36-3a63-4e1c-ac69-c095588f5967", + "name": "locale", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "205d9dce-b6c8-4b1d-9c9c-fa24788651cf", + "name": "picture", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "638216c8-ea8c-40e3-9429-771e9278920e", + "name": "gender", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "39c17eae-8ea7-422c-ae21-b8876bf12184", + "name": "birthdate", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "01c559cf-94f2-46ad-b965-3b2e1db1a2a6", + "name": "updated at", + "protocol": "openid-connect", + "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": "String" + } + }, + { + "id": "1693b5ab-28eb-485d-835d-2ae070ccb3ba", + "name": "profile", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "a0e08332-954c-46d2-9795-56eb31132580", + "name": "given name", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-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" + } + }, + { + "id": "cea0cd9c-d085-4d19-acc3-4bb41c891b68", + "name": "nickname", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "3122097d-4cba-46c2-8b3b-5d87a4cc605e", + "name": "middle name", + "protocol": "openid-connect", + "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" + } + }, + { + "id": "a3b97897-d913-4e0a-a4cf-033ce78f7d24", + "name": "username", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-property-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" + } + }, + { + "id": "a44eeb9d-410d-49c5-b0e0-5d84787627ad", + "name": "website", + "protocol": "openid-connect", + "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" + } + } + ] + }, + { + "id": "651408a7-6704-4198-a60f-988821b633ea", + "name": "role_list", + "description": "SAML role list", + "protocol": "saml", + "attributes": { + "consent.screen.text": "${samlRoleListScopeConsentText}", + "display.on.consent.screen": "true" + }, + "protocolMappers": [ + { + "id": "a8c56c7b-ccbc-4b01-8df5-3ecb6328755f", + "name": "role list", + "protocol": "saml", + "protocolMapper": "saml-role-list-mapper", + "consentRequired": false, + "config": { + "single": "false", + "attribute.nameformat": "Basic", + "attribute.name": "Role" + } + } + ] + }, + { + "id": "13ec0fd3-e64a-4d6f-9be7-c8760f2c9d6b", + "name": "roles", + "description": "OpenID Connect scope for add user roles to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "true", + "consent.screen.text": "${rolesScopeConsentText}" + }, + "protocolMappers": [ + { + "id": "75e741f8-dcd5-49d2-815e-8604ec1d08a1", + "name": "realm roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-realm-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "realm_access.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "06a2d506-4996-4a33-8c43-2cf64af6a630", + "name": "client roles", + "protocol": "openid-connect", + "protocolMapper": "oidc-usermodel-client-role-mapper", + "consentRequired": false, + "config": { + "user.attribute": "foo", + "access.token.claim": "true", + "claim.name": "resource_access.${client_id}.roles", + "jsonType.label": "String", + "multivalued": "true" + } + }, + { + "id": "3c3470df-d414-4e1c-87fc-3fb3cea34b8d", + "name": "audience resolve", + "protocol": "openid-connect", + "protocolMapper": "oidc-audience-resolve-mapper", + "consentRequired": false, + "config": {} + } + ] + }, + { + "id": "d85aba25-c74b-49e3-9ccb-77b4bb16efa5", + "name": "web-origins", + "description": "OpenID Connect scope for add allowed web origins to the access token", + "protocol": "openid-connect", + "attributes": { + "include.in.token.scope": "false", + "display.on.consent.screen": "false", + "consent.screen.text": "" + }, + "protocolMappers": [ + { + "id": "86b3f64f-1525-4500-bcbc-9b889b25f995", + "name": "allowed web origins", + "protocol": "openid-connect", + "protocolMapper": "oidc-allowed-origins-mapper", + "consentRequired": false, + "config": {} + } + ] + } + ], + "defaultDefaultClientScopes": [ + "roles", + "profile", + "role_list", + "email", + "web-origins" + ], + "defaultOptionalClientScopes": [ + "phone", + "address", + "offline_access", + "microprofile-jwt" + ], + "browserSecurityHeaders": { + "contentSecurityPolicyReportOnly": "", + "xContentTypeOptions": "nosniff", + "xRobotsTag": "none", + "xFrameOptions": "SAMEORIGIN", + "xXSSProtection": "1; mode=block", + "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "strictTransportSecurity": "max-age=31536000; includeSubDomains" + }, + "smtpServer": {}, + "eventsEnabled": false, + "eventsListeners": [ + "jboss-logging" + ], + "enabledEventTypes": [], + "adminEventsEnabled": false, + "adminEventsDetailsEnabled": false, + "components": { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ + { + "id": "59048b39-ad0f-4d12-8c52-7cfc2c43278a", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-full-name-mapper", + "oidc-sha256-pairwise-sub-mapper", + "saml-user-property-mapper", + "saml-role-list-mapper", + "oidc-address-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-usermodel-property-mapper" + ] + } + }, + { + "id": "760559a6-a59f-4175-9ac5-6f3612e20129", + "name": "Trusted Hosts", + "providerId": "trusted-hosts", + "subType": "anonymous", + "subComponents": {}, + "config": { + "host-sending-registration-request-must-match": [ + "true" + ], + "client-uris-must-match": [ + "true" + ] + } + }, + { + "id": "24f4cb42-76bd-499e-812a-4e0d270c9e13", + "name": "Full Scope Disabled", + "providerId": "scope", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "abbfc599-480a-44ef-8e33-73a83eaab166", + "name": "Allowed Protocol Mapper Types", + "providerId": "allowed-protocol-mappers", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allowed-protocol-mapper-types": [ + "saml-user-attribute-mapper", + "oidc-sha256-pairwise-sub-mapper", + "oidc-full-name-mapper", + "saml-role-list-mapper", + "saml-user-property-mapper", + "oidc-usermodel-property-mapper", + "oidc-usermodel-attribute-mapper", + "oidc-address-mapper" + ] + } + }, + { + "id": "3c6450f0-4521-402b-a247-c8165854b1fa", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "anonymous", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + }, + { + "id": "d9b64399-744b-498e-9d35-f68b1582bd7d", + "name": "Consent Required", + "providerId": "consent-required", + "subType": "anonymous", + "subComponents": {}, + "config": {} + }, + { + "id": "22f15f1f-3116-4348-a1e5-fc0d7576452a", + "name": "Max Clients Limit", + "providerId": "max-clients", + "subType": "anonymous", + "subComponents": {}, + "config": { + "max-clients": [ + "200" + ] + } + }, + { + "id": "4ad7b291-ddbb-4674-8c3d-ab8fd76d4168", + "name": "Allowed Client Scopes", + "providerId": "allowed-client-templates", + "subType": "authenticated", + "subComponents": {}, + "config": { + "allow-default-scopes": [ + "true" + ] + } + } + ], + "org.keycloak.keys.KeyProvider": [ + { + "id": "f71cc325-9907-4d27-a0e6-88fca7450e5e", + "name": "aes-generated", + "providerId": "aes-generated", + "subComponents": {}, + "config": { + "kid": [ + "6c7d982e-372f-49c6-a4f3-5c451fb85eca" + ], + "secret": [ + "yH6M3W7aOgh2_cKJ0srWbw" + ], + "priority": [ + "100" + ] + } + }, + { + "id": "7b50d0ab-dda5-4624-aa42-b4b397724ce1", + "name": "hmac-generated", + "providerId": "hmac-generated", + "subComponents": {}, + "config": { + "kid": [ + "587f0fb5-845d-4b45-87a0-84145092aaef" + ], + "secret": [ + "PuH8Lxh9GeNfGJRDk34SWIlBDdrJpC3U3SfcxqqQtlIf2DBzRKUu8VbDVrmMN5b5CoPsJhrQ2SVb-iE9Lzsb3A" + ], + "priority": [ + "100" + ], + "algorithm": [ + "HS256" + ] + } + }, + { + "id": "547c1c71-9f97-4e12-801b-ed5c2cc61bba", + "name": "rsa-generated", + "providerId": "rsa-generated", + "subComponents": {}, + "config": { + "privateKey": [ + "MIIEowIBAAKCAQEAjdo2HZ5ruNnIbkSeAfFYpbPvJw3vtz/VuKJerC4mUXYd7qRMhs3VLJZ3mFyeCuO8W81vkGrFiC9KQnX2lHj2dtA/RWEJw5bpz+JdOFr5pvXg0lQ0sa+hro9afWDygTU4FmLsEi5z98847TbH178RT6n7+JVqZ9jYU9rSpwVTC8E/4yxSuStmhGCcAkZ6dGhHNBdvGUgwxKYj7dYLRJiI+nilIdKuxPzxI/YZxZnXBHDdbNXJgDymTQPut99OnBxeZbH38CJ1MNo3VdV1fzOMGUHe+vn/EOD5E+pXC8PwvJnWU+XHUTFVZeyIXehh3pYLUsq/6bZ1MYsEaFIhznOkwwIDAQABAoIBAHB+64fVyUxRurhoRn737fuLlU/9p2xGfbHtYvNdrhnQeLB3MBGAT10K/1Gfsd6k+Q49AAsiAgGcr2HBt4nL3HohcOwOpvWsS0UIGjHFRFP6jw9+pEN+K9UJ7xObvPZnRFHMpbdNi76tYlINrbMV3h61ihR8OmSc/gKSeZjnihK5OkaNnlqGRaBM/koI+iAxUHuJPnBLBZmD4T8eIfE4S2TvUeVeQogI9Muvnb9tIPJ5XyP9iXWLdRjnek/+wTdxHHZuo06Tc0bMjRaTHiF6K9ntOM2EmQb6bS2J47zgzRLNFE22BWH7RJq659EzElkOn0C0k7dWDTur/3Lpx1+zxJECgYEA8t+J3J+9oGTFmY2VPH05Yr/R/iVDOyIOlO1CmgonOQ3KPhbfNBB3aTAJP20LOZChN4JoWuiZJg4UwzXOeY9DvdDkPO0YLlSjPAIwJNk+xcxFcp5hqMUul2db+cgEY8zp0Wg9kFOq3JmJjK4+1+fgsVnOB+B08ZYI6bZzsUVKzucCgYEAlYTrsxs6fQua0cvZNQPYNZzwF3LVwPBl/ntkdRBE3HGyZeCAhIj7e9pAUusCPsQJaCmW2UEmywD/aIxGrBkojzTKItshM3PN1PYKL8W0Zq+H67uF5KfdvsbabZWHfP/LGCpoKF8Ov7JVPPqGrZ03Z2SheeLZAtNeHN4OB1u9i8UCgYATkS7qN3Rvl67T0DRVy0D0U7/3Wckw2m2SUgsrneXLEvFYTz9sUmdMcjJMidx9pslWT4tYx6SPDFNf5tXbtU8f29SHlBJ+qRL9oq9+SIJmLS7rLRdxIXG/gPRIC3VPFRNBa8SJ/DOn0jbivqcRffz8TN/sgojpbc0KB0kK3ypHwQKBgCKVCcb1R0PgyUA4+9YNO5a647UotFPZxl1jwMpqpuKt0WtKz67X2AK/ah1DidNmmB5lcCRzsztE0c4mk7n+X6kvtoj1UeqKoFLfTV/bRGxzsOZPCxrl0J3tdFvgN+QrbZf7Rvf/dHPWFWzzLO8+66+YUNjWJQdIR/45Rdlh2KdZAoGBAMfF3ir+fe3KdQ6hAf9QyrLxJ5l+GO+IgtxXGbon7eeJBIZHHdMeDy4pC7DMcI214BmIntbyY+xS+gI3oM26EJUVmrZ6tkyIDFsCHm9rcXG9ogvffzQWM1Wqzm27hR/3s+EPWW9AOcIimiFV1UPp/mLjnrCuq58V2aJS/TT14oLe" + ], + "certificate": [ + "MIICmzCCAYMCBgFygL/j4DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjAwNjA0MTkxMDU4WhcNMzAwNjA0MTkxMjM4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCN2jYdnmu42chuRJ4B8Vils+8nDe+3P9W4ol6sLiZRdh3upEyGzdUslneYXJ4K47xbzW+QasWIL0pCdfaUePZ20D9FYQnDlunP4l04Wvmm9eDSVDSxr6Guj1p9YPKBNTgWYuwSLnP3zzjtNsfXvxFPqfv4lWpn2NhT2tKnBVMLwT/jLFK5K2aEYJwCRnp0aEc0F28ZSDDEpiPt1gtEmIj6eKUh0q7E/PEj9hnFmdcEcN1s1cmAPKZNA+63306cHF5lsffwInUw2jdV1XV/M4wZQd76+f8Q4PkT6lcLw/C8mdZT5cdRMVVl7Ihd6GHelgtSyr/ptnUxiwRoUiHOc6TDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIAqydMYxa51kNEyfXyR2kStlglE4LDeLBLHDABeBPE0eN2awoH/mw3kXS4OA/C0e3c7bAwViOzOVERGeUNiBvP5rL1Amuu97nwFcxhkTaJH4ZwCGkxceaIo9LNDpAEesqHLQSdplFXIA4TbEFoKMem4k31KVU7i9/rUesrSRmxLptIOK7LLvRMYiY/t7tdAvoZAtoliuQlFKQywEuxXQrCkcoVEAARABWGt0rsWC2xK0tVxHRIrENwvMp/aUYd17sZ0403aaS9dlvfQ63ExnaHd+++RJtPku8P220Tw27YVmFAwzJgS0aUpEaDsgRNz6OMSyxEg/n7eKK08aU3szwQ=" + ], + "priority": [ + "100" + ] + } + } + ] + }, + "internationalizationEnabled": false, + "supportedLocales": [], + "authenticationFlows": [ + { + "id": "3253f9b7-905d-4458-ad8a-8ada5e16d195", + "alias": "Account verification options", + "description": "Method with which to verity the existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-email-verification", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Verify Existing Account by Re-authentication", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "75bd854e-ab99-46f1-90ed-a8bfc1559558", + "alias": "Authentication Options", + "description": "Authentication options.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "basic-auth", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "basic-auth-otp", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "9b0e6cce-62c5-4fb6-a48d-e07c950e38c3", + "alias": "Browser - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "1c26fd14-ac06-4dc1-bdd8-8c34c1b41720", + "alias": "Direct Grant - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "254f7549-51ec-4565-a736-35c07b6e25f0", + "alias": "First broker login - Conditional OTP", + "description": "Flow to determine if the OTP is required for the authentication", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-otp-form", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "b2413da8-3de9-4bfe-b77e-643fd1964c8f", + "alias": "Handle Existing Account", + "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-confirm-link", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Account verification options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "f8392bfb-8dce-4a16-8af1-b2a4d1a0a273", + "alias": "Reset - Conditional OTP", + "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "conditional-user-configured", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-otp", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "fb69c297-b26e-44fa-aabd-d7b40eec3cd3", + "alias": "User creation or linking", + "description": "Flow for the existing/non-existing user alternatives", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "create unique user config", + "authenticator": "idp-create-user-if-unique", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 20, + "flowAlias": "Handle Existing Account", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "de3a41a9-7018-4931-9c4d-d04f9501b2ce", + "alias": "Verify Existing Account by Re-authentication", + "description": "Reauthentication of existing account", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "idp-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "First broker login - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "6526b0d1-b48e-46c6-bb08-11ebcf458def", + "alias": "browser", + "description": "browser based authentication", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-cookie", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "auth-spnego", + "requirement": "DISABLED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "identity-provider-redirector", + "requirement": "ALTERNATIVE", + "priority": 25, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "ALTERNATIVE", + "priority": 30, + "flowAlias": "forms", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "92a653ba-8f2d-4283-8354-ca55f9d89181", + "alias": "clients", + "description": "Base authentication for clients", + "providerId": "client-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "client-secret", + "requirement": "ALTERNATIVE", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-jwt", + "requirement": "ALTERNATIVE", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-secret-jwt", + "requirement": "ALTERNATIVE", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "client-x509", + "requirement": "ALTERNATIVE", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "e365be39-78db-46f0-b2e8-4e7001c2f5d0", + "alias": "direct grant", + "description": "OpenID Connect Resource Owner Grant", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "direct-grant-validate-username", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "direct-grant-validate-password", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 30, + "flowAlias": "Direct Grant - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "dd61caf5-a40f-48b7-9e8c-a1f3b67041dd", + "alias": "docker auth", + "description": "Used by Docker clients to authenticate against the IDP", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "docker-http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "7a055643-62e1-4ac1-b126-9a8d6c299635", + "alias": "first broker login", + "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticatorConfig": "review profile config", + "authenticator": "idp-review-profile", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "User creation or linking", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fe8bc7ee-6e8f-436e-8336-c60fcd350843", + "alias": "forms", + "description": "Username, password, otp and other auth forms.", + "providerId": "basic-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "auth-username-password-form", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 20, + "flowAlias": "Browser - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "3646f08e-ab70-415b-a701-6ed2e2d214c9", + "alias": "http challenge", + "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "no-cookie-redirect", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "REQUIRED", + "priority": 20, + "flowAlias": "Authentication Options", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "04176530-0972-47ad-83df-19d8534caac2", + "alias": "registration", + "description": "registration flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-page-form", + "requirement": "REQUIRED", + "priority": 10, + "flowAlias": "registration form", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "fa0ed569-6746-439e-b07e-89f7ed918c07", + "alias": "registration form", + "description": "registration form", + "providerId": "form-flow", + "topLevel": false, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "registration-user-creation", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-profile-action", + "requirement": "REQUIRED", + "priority": 40, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-password-action", + "requirement": "REQUIRED", + "priority": 50, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "registration-recaptcha-action", + "requirement": "DISABLED", + "priority": 60, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + }, + { + "id": "03680917-28f3-4ccd-bdf6-4a516f7c0018", + "alias": "reset credentials", + "description": "Reset credentials for a user if they forgot their password or something", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "reset-credentials-choose-user", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-credential-email", + "requirement": "REQUIRED", + "priority": 20, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "authenticator": "reset-password", + "requirement": "REQUIRED", + "priority": 30, + "userSetupAllowed": false, + "autheticatorFlow": false + }, + { + "requirement": "CONDITIONAL", + "priority": 40, + "flowAlias": "Reset - Conditional OTP", + "userSetupAllowed": false, + "autheticatorFlow": true + } + ] + }, + { + "id": "19a9d9aa-2d2b-4701-807f-c384ab921c7e", + "alias": "saml ecp", + "description": "SAML ECP Profile Authentication Flow", + "providerId": "basic-flow", + "topLevel": true, + "builtIn": true, + "authenticationExecutions": [ + { + "authenticator": "http-basic-authenticator", + "requirement": "REQUIRED", + "priority": 10, + "userSetupAllowed": false, + "autheticatorFlow": false + } + ] + } + ], + "authenticatorConfig": [ + { + "id": "534f01f4-45b3-43a0-91d1-238860cc126d", + "alias": "create unique user config", + "config": { + "require.password.update.after.registration": "false" + } + }, + { + "id": "65bb9337-9633-4a21-8f6f-1d4129f664ac", + "alias": "review profile config", + "config": { + "update.profile.on.first.login": "missing" + } + } + ], + "requiredActions": [ + { + "alias": "CONFIGURE_TOTP", + "name": "Configure OTP", + "providerId": "CONFIGURE_TOTP", + "enabled": true, + "defaultAction": false, + "priority": 10, + "config": {} + }, + { + "alias": "terms_and_conditions", + "name": "Terms and Conditions", + "providerId": "terms_and_conditions", + "enabled": false, + "defaultAction": false, + "priority": 20, + "config": {} + }, + { + "alias": "UPDATE_PASSWORD", + "name": "Update Password", + "providerId": "UPDATE_PASSWORD", + "enabled": true, + "defaultAction": false, + "priority": 30, + "config": {} + }, + { + "alias": "UPDATE_PROFILE", + "name": "Update Profile", + "providerId": "UPDATE_PROFILE", + "enabled": true, + "defaultAction": false, + "priority": 40, + "config": {} + }, + { + "alias": "VERIFY_EMAIL", + "name": "Verify Email", + "providerId": "VERIFY_EMAIL", + "enabled": true, + "defaultAction": false, + "priority": 50, + "config": {} + }, + { + "alias": "update_user_locale", + "name": "Update User Locale", + "providerId": "update_user_locale", + "enabled": true, + "defaultAction": false, + "priority": 1000, + "config": {} + } + ], + "browserFlow": "browser", + "registrationFlow": "registration", + "directGrantFlow": "direct grant", + "resetCredentialsFlow": "reset credentials", + "clientAuthenticationFlow": "clients", + "dockerAuthenticationFlow": "docker auth", + "attributes": {}, + "keycloakVersion": "10.0.0", + "userManagedAccessAllowed": false +} diff --git a/keycloak/oauth2-proxy-users-0.json b/keycloak/oauth2-proxy-users-0.json new file mode 100644 index 00000000000..2a46504db24 --- /dev/null +++ b/keycloak/oauth2-proxy-users-0.json @@ -0,0 +1,38 @@ +{ + "realm": "oauth2-proxy", + "users": [ + { + "id": "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", + "createdTimestamp": 1591297959169, + "username": "builtin-nick", + "email": "nick@malinator.com", + "enabled": true, + "totp": false, + "emailVerified": true, + "credentials": [ + { + "id": "a1a06ecd-fdc0-4e67-92cd-2da22d724e32", + "type": "password", + "createdDate": 1591297959315, + "secretData": "{\"value\":\"6rt5zuqHVHopvd0FTFE0CYadXTtzY0mDY2BrqnNQGS51/7DfMJeGgj0roNnGMGvDv30imErNmiSOYl+cL9jiIA==\",\"salt\":\"LI0kqr09JB7J9wvr2Hxzzg==\"}", + "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" + } + ], + "disableableCredentialTypes": [], + "requiredActions": [], + "realmRoles": [ + "offline_access", + "admin", + "uma_authorization" + ], + "clientRoles": { + "account": [ + "view-profile", + "manage-account" + ] + }, + "notBefore": 0, + "groups": [] + } + ] +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 3257a3cc7ac..3548c7151e1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -36,8 +36,10 @@ import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; +import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; +import jakarta.inject.Inject; import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.persistence.EntityManager; @@ -234,6 +236,9 @@ String getWrappedMessageWhenJson() { @EJB GuestbookResponseServiceBean gbRespSvc; + @Inject + OpenIdContext openIdContext; + @PersistenceContext(unitName = "VDCNet-ejbPU") protected EntityManager em; @@ -322,7 +327,17 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon if (requestUser.isAuthenticated()) { return (AuthenticatedUser) requestUser; } else { - throw new WrappedResponse(authenticatedUserRequired()); + try { + final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + if (authUser != null) { + return authUser; + } else { + throw new WrappedResponse(authenticatedUserRequired()); + } + } catch (final Exception ignore) { + throw new WrappedResponse(authenticatedUserRequired()); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java new file mode 100644 index 00000000000..f9a51c3b2ac --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java @@ -0,0 +1,34 @@ +package edu.harvard.iq.dataverse.api; + +import java.io.IOException; + +import fish.payara.security.annotations.OpenIdAuthenticationDefinition; +import jakarta.annotation.security.DeclareRoles; +import jakarta.ejb.EJB; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.HttpConstraint; +import jakarta.servlet.annotation.ServletSecurity; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/oidc/login") +@OpenIdAuthenticationDefinition( + providerURI="#{openIdConfigBean.providerURI}", + clientId="#{openIdConfigBean.clientId}", + clientSecret="#{openIdConfigBean.clientSecret}", + redirectURI="#{openIdConfigBean.redirectURI}", + scope="email" +) +@DeclareRoles("all") +@ServletSecurity(@HttpConstraint(rolesAllowed = "all")) +public class OpenIDAuthentication extends HttpServlet { + @EJB + OpenIDConfigBean openIdConfigBean; + + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + response.getWriter().println("..."); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java new file mode 100644 index 00000000000..804cc7ea5d5 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -0,0 +1,48 @@ +package edu.harvard.iq.dataverse.api; + +import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import fish.payara.security.openid.api.OpenIdContext; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.json.JsonObjectBuilder; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.container.ContainerRequestContext; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.Response; + +@Stateless +@Path("callback") +public class OpenIDCallback extends AbstractApiBean { + @Inject + OpenIdContext openIdContext; + + @Inject + protected AuthenticationServiceBean authSvc; + + @Path("token") + @GET + public Response token(@Context ContainerRequestContext crc) { + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); + } + + @Path("session") + @GET + public Response session(@Context ContainerRequestContext crc) { + final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + final JsonObjectBuilder userJson; + if (authUser != null) { + userJson = authUser.toJson(); + } else { + userJson = null; + } + return ok( + jsonObjectBuilder() + .add("user", userJson) + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + ); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java new file mode 100644 index 00000000000..dcf9b575073 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -0,0 +1,25 @@ +package edu.harvard.iq.dataverse.api; + +import edu.harvard.iq.dataverse.settings.JvmSettings; +import jakarta.ejb.Stateless; +import jakarta.inject.Named; + +@Stateless +@Named("openIdConfigBean") +public class OpenIDConfigBean implements java.io.Serializable { + public String getProviderURI() { + return JvmSettings.API_OIDC_PROVIDER_URI.lookup(); + } + + public String getClientId() { + return JvmSettings.API_OIDC_CLIENT_ID.lookup(); + } + + public String getClientSecret() { + return JvmSettings.API_OIDC_CLIENT_SECRET.lookup(); + } + + public String getRedirectURI() { + return JvmSettings.API_OIDC_REDIRECT_URI.lookup(); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index d7eea970b8a..282fab901e4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -235,6 +235,13 @@ public enum JvmSettings { OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"), OIDC_PKCE_CACHE_MAXSIZE(SCOPE_OIDC_PKCE, "max-cache-size"), OIDC_PKCE_CACHE_MAXAGE(SCOPE_OIDC_PKCE, "max-cache-age"), + // AUTH: OPEN_ID SETTINGS + SCOPE_AUTH_API(SCOPE_AUTH, "api"), + SCOPE_OPEN_ID(SCOPE_AUTH_API, "oidc"), + API_OIDC_PROVIDER_URI(SCOPE_OPEN_ID, "provider-uri"), + API_OIDC_CLIENT_ID(SCOPE_OPEN_ID, "client-id"), + API_OIDC_CLIENT_SECRET(SCOPE_OPEN_ID, "client-secret"), + API_OIDC_REDIRECT_URI(SCOPE_OPEN_ID, "redirect-uri"), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), From 96fab764c468d190d57e9eaba22cbabc59869b5e Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 17:37:33 +0200 Subject: [PATCH 02/61] replaced tabs with spaces --- .../iq/dataverse/api/OpenIDCallback.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 804cc7ea5d5..e9f5ba06c85 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -19,30 +19,30 @@ public class OpenIDCallback extends AbstractApiBean { @Inject OpenIdContext openIdContext; - @Inject + @Inject protected AuthenticationServiceBean authSvc; - @Path("token") - @GET - public Response token(@Context ContainerRequestContext crc) { - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); - } + @Path("token") + @GET + public Response token(@Context ContainerRequestContext crc) { + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); + } - @Path("session") - @GET - public Response session(@Context ContainerRequestContext crc) { - final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); - final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); - final JsonObjectBuilder userJson; + @Path("session") + @GET + public Response session(@Context ContainerRequestContext crc) { + final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + final JsonObjectBuilder userJson; if (authUser != null) { - userJson = authUser.toJson(); + userJson = authUser.toJson(); } else { - userJson = null; - } - return ok( - jsonObjectBuilder() - .add("user", userJson) - .add("session", crc.getCookies().get("JSESSIONID").getValue()) - ); - } + userJson = null; + } + return ok( + jsonObjectBuilder() + .add("user", userJson) + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + ); + } } From cbde18f478992a543c2d45bde4dbcea19662cc6c Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 19:57:20 +0200 Subject: [PATCH 03/61] better error handling for not authenticated users --- .../iq/dataverse/api/OpenIDCallback.java | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index e9f5ba06c85..9184b3a9578 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -31,18 +31,21 @@ public Response token(@Context ContainerRequestContext crc) { @Path("session") @GET public Response session(@Context ContainerRequestContext crc) { - final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); - final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); - final JsonObjectBuilder userJson; - if (authUser != null) { - userJson = authUser.toJson(); - } else { - userJson = null; + try { + final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + final JsonObjectBuilder userJson; + if (authUser != null) { + userJson = authUser.toJson(); + } else { + userJson = null; + } + return ok( + jsonObjectBuilder() + .add("user", userJson) + .add("session", crc.getCookies().get("JSESSIONID").getValue())); + } catch (final Exception ignore) { + return authenticatedUserRequired(); } - return ok( - jsonObjectBuilder() - .add("user", userJson) - .add("session", crc.getCookies().get("JSESSIONID").getValue()) - ); } } From b137bbcf871a48241254d75041fc03531421f59e Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Thu, 3 Oct 2024 20:35:19 +0200 Subject: [PATCH 04/61] Update docker-compose-dev.yml Thanks! Co-authored-by: Philip Durbin --- docker-compose-dev.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 2d7744fab58..bb263900ce2 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -174,7 +174,7 @@ services: volumes: - ./keycloak:/opt/keycloak/data/import environment: - KC_HTTP_PORT: 9080 + KC_HTTP_PORT: "9080" KEYCLOAK_ADMIN: admin@kuleuven.be KEYCLOAK_ADMIN_PASSWORD: password ports: From e94ebc45ed441e97c206b91d9b6e7f42c5d78df6 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 21:05:49 +0200 Subject: [PATCH 05/61] changed the demo user to admin and better error when user is not found in dv --- keycloak/oauth2-proxy-users-0.json | 4 ++-- .../edu/harvard/iq/dataverse/api/OpenIDCallback.java | 12 +++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/keycloak/oauth2-proxy-users-0.json b/keycloak/oauth2-proxy-users-0.json index 2a46504db24..4676632bb89 100644 --- a/keycloak/oauth2-proxy-users-0.json +++ b/keycloak/oauth2-proxy-users-0.json @@ -4,8 +4,8 @@ { "id": "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", "createdTimestamp": 1591297959169, - "username": "builtin-nick", - "email": "nick@malinator.com", + "username": "admin", + "email": "dataverse-admin@mailinator.com", "enabled": true, "totp": false, "emailVerified": true, diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 9184b3a9578..969291b0f5a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -34,16 +34,14 @@ public Response session(@Context ContainerRequestContext crc) { try { final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); - final JsonObjectBuilder userJson; if (authUser != null) { - userJson = authUser.toJson(); + return ok( + jsonObjectBuilder() + .add("user", authUser.toJson()) + .add("session", crc.getCookies().get("JSESSIONID").getValue())); } else { - userJson = null; + return notFound("user with email " + email + " not found"); } - return ok( - jsonObjectBuilder() - .add("user", userJson) - .add("session", crc.getCookies().get("JSESSIONID").getValue())); } catch (final Exception ignore) { return authenticatedUserRequired(); } From 2a2c5830945844570e7c83d497d25ff20e63a4f6 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 21:10:44 +0200 Subject: [PATCH 06/61] admin user email fix --- keycloak/oauth2-proxy-users-0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/keycloak/oauth2-proxy-users-0.json b/keycloak/oauth2-proxy-users-0.json index 4676632bb89..efcffddb7d8 100644 --- a/keycloak/oauth2-proxy-users-0.json +++ b/keycloak/oauth2-proxy-users-0.json @@ -5,7 +5,7 @@ "id": "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", "createdTimestamp": 1591297959169, "username": "admin", - "email": "dataverse-admin@mailinator.com", + "email": "dataverse@mailinator.com", "enabled": true, "totp": false, "emailVerified": true, From b7937e653a41eec6688e37548575733b28b54de7 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 21:46:55 +0200 Subject: [PATCH 07/61] restored kaycloak config to original --- conf/keycloak/test-realm.json | 2 +- docker-compose-dev.yml | 40 +- keycloak/oauth2-proxy-realm.json | 2071 ---------------------------- keycloak/oauth2-proxy-users-0.json | 38 - 4 files changed, 23 insertions(+), 2128 deletions(-) delete mode 100644 keycloak/oauth2-proxy-realm.json delete mode 100644 keycloak/oauth2-proxy-users-0.json diff --git a/conf/keycloak/test-realm.json b/conf/keycloak/test-realm.json index efe71cc5d29..5dc0bd6d6ee 100644 --- a/conf/keycloak/test-realm.json +++ b/conf/keycloak/test-realm.json @@ -398,7 +398,7 @@ "emailVerified" : true, "firstName" : "Dataverse", "lastName" : "Admin", - "email" : "dataverse-admin@mailinator.com", + "email" : "dataverse@mailinator.com", "credentials" : [ { "id" : "28f1ece7-26fb-40f1-9174-5ffce7b85c0a", "type" : "password", diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index bb263900ce2..8c49cc4f502 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -18,9 +18,13 @@ services: DATAVERSE_JSF_REFRESH_PERIOD: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" - DATAVERSE_AUTH_API_OIDC_CLIENT_ID: oauth2-proxy - DATAVERSE_AUTH_API_OIDC_CLIENT_SECRET: 72341b6d-7065-4518-a0e4-50ee15025608 - DATAVERSE_AUTH_API_OIDC_PROVIDER_URI: http://172.17.0.1:9080/realms/oauth2-proxy + DATAVERSE_AUTH_OIDC_ENABLED: "1" + DATAVERSE_AUTH_OIDC_CLIENT_ID: test + DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 + DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test + DATAVERSE_AUTH_API_OIDC_CLIENT_ID: test + DATAVERSE_AUTH_API_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 + DATAVERSE_AUTH_API_OIDC_PROVIDER_URI: http://keycloak.mydomain.com:8090/realms/test DATAVERSE_AUTH_API_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters" # These two oai settings are here to get HarvestingServerIT to pass @@ -163,24 +167,24 @@ services: tmpfs: - /mail:mode=770,size=128M,uid=1000,gid=1000 - keycloak: - container_name: keycloak - image: keycloak/keycloak:25.0 + dev_keycloak: + container_name: "dev_keycloak" + image: 'quay.io/keycloak/keycloak:21.0' hostname: keycloak - command: - - 'start-dev' - - '--http-port=9080' - - '--import-realm' - volumes: - - ./keycloak:/opt/keycloak/data/import environment: - KC_HTTP_PORT: "9080" - KEYCLOAK_ADMIN: admin@kuleuven.be - KEYCLOAK_ADMIN_PASSWORD: password - ports: - - 9080:9080 + - KEYCLOAK_ADMIN=kcadmin + - KEYCLOAK_ADMIN_PASSWORD=kcpassword + - KEYCLOAK_LOGLEVEL=DEBUG + - KC_HOSTNAME_STRICT=false networks: - - dataverse + dataverse: + aliases: + - keycloak.mydomain.com #create a DNS alias within the network (add the same alias to your /etc/hosts to get a working OIDC flow) + command: start-dev --import-realm --http-port=8090 # change port to 8090, so within the network and external the same port is used + ports: + - "8090:8090" + volumes: + - './conf/keycloak/test-realm.json:/opt/keycloak/data/import/test-realm.json' # This proxy configuration is only intended to be used for development purposes! # DO NOT USE IN PRODUCTION! HIGH SECURITY RISK! diff --git a/keycloak/oauth2-proxy-realm.json b/keycloak/oauth2-proxy-realm.json deleted file mode 100644 index ac69b2375fd..00000000000 --- a/keycloak/oauth2-proxy-realm.json +++ /dev/null @@ -1,2071 +0,0 @@ -{ - "id": "oauth2-proxy", - "realm": "oauth2-proxy", - "displayName": "Keycloak", - "displayNameHtml": "
Keycloak
", - "notBefore": 0, - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 60, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "32626c92-4327-40f1-b318-76a6b5c7eee5", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "oauth2-proxy", - "attributes": {} - }, - { - "id": "e36da570-7ae0-4323-8b39-73eb92ce722f", - "name": "admin", - "description": "${role_admin}", - "composite": true, - "composites": { - "realm": [ - "create-realm" - ], - "client": { - "oauth2-proxy-realm": [ - "query-groups", - "create-client", - "query-realms", - "view-authorization", - "view-realm", - "manage-clients", - "query-users", - "manage-realm", - "view-events", - "manage-events", - "view-identity-providers", - "view-users", - "manage-identity-providers", - "manage-authorization", - "manage-users", - "view-clients", - "query-clients", - "impersonation" - ] - } - }, - "clientRole": false, - "containerId": "oauth2-proxy", - "attributes": {} - }, - { - "id": "71aca46c-6fcf-4456-ba87-6374e70108a2", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "oauth2-proxy", - "attributes": {} - }, - { - "id": "6ca3fee8-1a3f-4068-a311-6e81223a884b", - "name": "create-realm", - "description": "${role_create-realm}", - "composite": false, - "clientRole": false, - "containerId": "oauth2-proxy", - "attributes": {} - } - ], - "client": { - "oauth2-proxy": [], - "security-admin-console": [], - "admin-cli": [], - "account-console": [], - "broker": [ - { - "id": "2cc5e40c-0a28-4c09-85eb-20cd47ac1351", - "name": "read-token", - "description": "${role_read-token}", - "composite": false, - "clientRole": true, - "containerId": "380985f1-61c7-4940-93ae-7a09458071ca", - "attributes": {} - } - ], - "oauth2-proxy-realm": [ - { - "id": "a8271c2c-6437-4ca5-ae83-49ea5fe1318d", - "name": "query-groups", - "description": "${role_query-groups}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "5a7cb1ae-7dac-486b-bf7b-4d7fbc5adb31", - "name": "create-client", - "description": "${role_create-client}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "a9e6a2fa-c31b-4959-bf8a-a46fcc9c65ec", - "name": "view-authorization", - "description": "${role_view-authorization}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "1cef34e3-569a-4d2b-ba5c-aafe5c7ab423", - "name": "query-realms", - "description": "${role_query-realms}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "efc46075-30cd-4600-aa92-2ae4a171d0c2", - "name": "view-realm", - "description": "${role_view-realm}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "9ffacaf0-afc6-49e9-8708-ef35ac40f3f8", - "name": "manage-clients", - "description": "${role_manage-clients}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "90662091-b3bc-4ae4-83c9-a4f53e7e9eeb", - "name": "query-users", - "description": "${role_query-users}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "9a5fbc9d-6fae-4155-86f6-72fd399aa126", - "name": "manage-realm", - "description": "${role_manage-realm}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "03f46127-9436-477d-8c7f-58569f45237c", - "name": "view-events", - "description": "${role_view-events}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "f10eaea2-90ab-4310-9d5f-8d986564d061", - "name": "view-identity-providers", - "description": "${role_view-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "2403e038-2cf7-4b06-b5cb-33a417a00d8d", - "name": "manage-events", - "description": "${role_manage-events}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "677d057b-66f8-4163-9948-95fdbd06dfdc", - "name": "view-users", - "description": "${role_view-users}", - "composite": true, - "composites": { - "client": { - "oauth2-proxy-realm": [ - "query-groups", - "query-users" - ] - } - }, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "dc140fa6-bf2c-49f2-b8c9-fc34ef8a2c63", - "name": "manage-identity-providers", - "description": "${role_manage-identity-providers}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "155bf234-4895-4855-95c2-a460518f57e8", - "name": "manage-authorization", - "description": "${role_manage-authorization}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "5441ec71-3eac-4696-9e68-0de54fbdde98", - "name": "manage-users", - "description": "${role_manage-users}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "2db0f052-cb91-4170-81fd-107756b162f7", - "name": "view-clients", - "description": "${role_view-clients}", - "composite": true, - "composites": { - "client": { - "oauth2-proxy-realm": [ - "query-clients" - ] - } - }, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "e1d7f235-8bf2-40b8-be49-49aca70a5088", - "name": "query-clients", - "description": "${role_query-clients}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - }, - { - "id": "e743f66a-2f56-4b97-b34b-33f06ff1e739", - "name": "impersonation", - "description": "${role_impersonation}", - "composite": false, - "clientRole": true, - "containerId": "7174c175-1887-4e57-b95b-969fe040deff", - "attributes": {} - } - ], - "account": [ - { - "id": "64d8f532-839e-4386-b2eb-fe8848b0a9de", - "name": "manage-consent", - "description": "${role_manage-consent}", - "composite": true, - "composites": { - "client": { - "account": [ - "view-consent" - ] - } - }, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - }, - { - "id": "3ec22748-960f-4f96-a43e-50f54a02dc23", - "name": "view-profile", - "description": "${role_view-profile}", - "composite": false, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - }, - { - "id": "177d18e4-46b0-4ea3-8b70-327486ce5bb2", - "name": "view-applications", - "description": "${role_view-applications}", - "composite": false, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - }, - { - "id": "703643d6-0542-4e27-9737-7c442925c18c", - "name": "manage-account-links", - "description": "${role_manage-account-links}", - "composite": false, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - }, - { - "id": "c64f9f66-d762-4337-8833-cf31c316e8a7", - "name": "view-consent", - "description": "${role_view-consent}", - "composite": false, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - }, - { - "id": "611f568b-0fdd-4d2e-ba34-03136cd486c4", - "name": "manage-account", - "description": "${role_manage-account}", - "composite": true, - "composites": { - "client": { - "account": [ - "manage-account-links" - ] - } - }, - "clientRole": true, - "containerId": "a367038f-fe01-4459-9f91-7ad0cf498533", - "attributes": {} - } - ] - } - }, - "groups": [], - "defaultRoles": [ - "offline_access", - "uma_authorization" - ], - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" - ], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "scopeMappings": [ - { - "clientScope": "offline_access", - "roles": [ - "offline_access" - ] - } - ], - "clientScopeMappings": { - "account": [ - { - "client": "account-console", - "roles": [ - "manage-account" - ] - } - ] - }, - "clients": [ - { - "id": "a367038f-fe01-4459-9f91-7ad0cf498533", - "clientId": "account", - "name": "${client_account}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/oauth2-proxy/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "0896a464-da81-4454-bee9-b56bdbad9e7f", - "defaultRoles": [ - "view-profile", - "manage-account" - ], - "redirectUris": [ - "/realms/oauth2-proxy/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "72f75604-1e21-407c-b967-790aafd11534", - "clientId": "account-console", - "name": "${client_account-console}", - "rootUrl": "${authBaseUrl}", - "baseUrl": "/realms/oauth2-proxy/account/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "91f85142-ee18-4e30-9949-e5acb701bdee", - "redirectUris": [ - "/realms/oauth2-proxy/account/*" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "2772c101-0dba-49b7-9627-5aaddc666939", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "b13fd0de-3be0-4a08-bc5d-d1de34421b1a", - "clientId": "admin-cli", - "name": "${client_admin-cli}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "4640af2e-b4a6-44eb-85ec-6278a62a4f01", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": false, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "380985f1-61c7-4940-93ae-7a09458071ca", - "clientId": "broker", - "name": "${client_broker}", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "65d2ba2b-bcae-49ff-9f56-77c818f55930", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "7174c175-1887-4e57-b95b-969fe040deff", - "clientId": "oauth2-proxy-realm", - "name": "oauth2-proxy Realm", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "40f73851-a94c-4091-90de-aeee8ca1acf8", - "redirectUris": [], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": true, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "attributes": {}, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": 0, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "0493c7c6-6e20-49ea-9acb-627c0b52d400", - "clientId": "oauth2-proxy", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "72341b6d-7065-4518-a0e4-50ee15025608", - "redirectUris": [ - "http://localhost:8080/api/v1/callback/token" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "exclude.session.state.from.auth.response": "false", - "saml_force_name_id_format": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - }, - { - "id": "2a3ad1fd-a30d-4b72-89c4-bed12f178338", - "clientId": "security-admin-console", - "name": "${client_security-admin-console}", - "rootUrl": "${authAdminUrl}", - "baseUrl": "/admin/oauth2-proxy/console/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "b234b7aa-8417-410f-b3fd-c57434d3aa4a", - "redirectUris": [ - "/admin/oauth2-proxy/console/*" - ], - "webOrigins": [ - "+" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": true, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "pkce.code.challenge.method": "S256" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": false, - "nodeReRegistrationTimeout": 0, - "protocolMappers": [ - { - "id": "5885b0d3-a917-4b52-8380-f37d0754a2ef", - "name": "locale", - "protocol": "openid-connect", - "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" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "role_list", - "roles", - "profile", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "47ea3b67-4f0c-4c7e-8ac6-a33a3d655894", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "4be0ca19-0ec7-4cc1-b263-845ea539ff12", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "aba72e57-540f-4825-95b7-2d143be028cc", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "7fe82724-5748-4b6d-9708-a028f5d3b970", - "name": "email verified", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "e42f334e-cfae-44a0-905d-c3ef215feaae", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-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" - } - } - ] - }, - { - "id": "ec765598-bd71-4318-86c3-b3f81a41c99e", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "90694036-4014-4672-a2c8-c68319e9308a", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-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" - } - }, - { - "id": "f7b0fcc0-6139-4158-ac45-34fd9a58a5ef", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "8a09267b-3634-4a9c-baab-6f2fb4137347", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "3a48c5dd-33a8-4be0-9d2e-30fd7f98363a", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "5427d1b4-ba79-412a-b23c-da640a98980c", - "name": "phone number", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "31d4a53f-6503-40e8-bd9d-79a7c46c4fbe", - "name": "phone number verified", - "protocol": "openid-connect", - "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" - } - } - ] - }, - { - "id": "5921a9e9-7fec-4471-95e3-dd96eebdec58", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "4fa92092-ee0d-4dc7-a63b-1e3b02d35ebb", - "name": "zoneinfo", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "1a5cc2e2-c983-4150-8583-23a7f5c826bf", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-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" - } - }, - { - "id": "67931f77-722a-492d-b581-a953e26b7d44", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "10f6ac36-3a63-4e1c-ac69-c095588f5967", - "name": "locale", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "205d9dce-b6c8-4b1d-9c9c-fa24788651cf", - "name": "picture", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "638216c8-ea8c-40e3-9429-771e9278920e", - "name": "gender", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "39c17eae-8ea7-422c-ae21-b8876bf12184", - "name": "birthdate", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "01c559cf-94f2-46ad-b965-3b2e1db1a2a6", - "name": "updated at", - "protocol": "openid-connect", - "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": "String" - } - }, - { - "id": "1693b5ab-28eb-485d-835d-2ae070ccb3ba", - "name": "profile", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "a0e08332-954c-46d2-9795-56eb31132580", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-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" - } - }, - { - "id": "cea0cd9c-d085-4d19-acc3-4bb41c891b68", - "name": "nickname", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "3122097d-4cba-46c2-8b3b-5d87a4cc605e", - "name": "middle name", - "protocol": "openid-connect", - "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" - } - }, - { - "id": "a3b97897-d913-4e0a-a4cf-033ce78f7d24", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-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" - } - }, - { - "id": "a44eeb9d-410d-49c5-b0e0-5d84787627ad", - "name": "website", - "protocol": "openid-connect", - "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" - } - } - ] - }, - { - "id": "651408a7-6704-4198-a60f-988821b633ea", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "a8c56c7b-ccbc-4b01-8df5-3ecb6328755f", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "false", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "13ec0fd3-e64a-4d6f-9be7-c8760f2c9d6b", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "75e741f8-dcd5-49d2-815e-8604ec1d08a1", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "06a2d506-4996-4a33-8c43-2cf64af6a630", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "3c3470df-d414-4e1c-87fc-3fb3cea34b8d", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - } - ] - }, - { - "id": "d85aba25-c74b-49e3-9ccb-77b4bb16efa5", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "86b3f64f-1525-4500-bcbc-9b889b25f995", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - } - ], - "defaultDefaultClientScopes": [ - "roles", - "profile", - "role_list", - "email", - "web-origins" - ], - "defaultOptionalClientScopes": [ - "phone", - "address", - "offline_access", - "microprofile-jwt" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "xXSSProtection": "1; mode=block", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "59048b39-ad0f-4d12-8c52-7cfc2c43278a", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-attribute-mapper", - "oidc-full-name-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-user-property-mapper", - "saml-role-list-mapper", - "oidc-address-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-usermodel-property-mapper" - ] - } - }, - { - "id": "760559a6-a59f-4175-9ac5-6f3612e20129", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "24f4cb42-76bd-499e-812a-4e0d270c9e13", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "abbfc599-480a-44ef-8e33-73a83eaab166", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "saml-user-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "oidc-full-name-mapper", - "saml-role-list-mapper", - "saml-user-property-mapper", - "oidc-usermodel-property-mapper", - "oidc-usermodel-attribute-mapper", - "oidc-address-mapper" - ] - } - }, - { - "id": "3c6450f0-4521-402b-a247-c8165854b1fa", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "d9b64399-744b-498e-9d35-f68b1582bd7d", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "22f15f1f-3116-4348-a1e5-fc0d7576452a", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "4ad7b291-ddbb-4674-8c3d-ab8fd76d4168", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "f71cc325-9907-4d27-a0e6-88fca7450e5e", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "kid": [ - "6c7d982e-372f-49c6-a4f3-5c451fb85eca" - ], - "secret": [ - "yH6M3W7aOgh2_cKJ0srWbw" - ], - "priority": [ - "100" - ] - } - }, - { - "id": "7b50d0ab-dda5-4624-aa42-b4b397724ce1", - "name": "hmac-generated", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "kid": [ - "587f0fb5-845d-4b45-87a0-84145092aaef" - ], - "secret": [ - "PuH8Lxh9GeNfGJRDk34SWIlBDdrJpC3U3SfcxqqQtlIf2DBzRKUu8VbDVrmMN5b5CoPsJhrQ2SVb-iE9Lzsb3A" - ], - "priority": [ - "100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "547c1c71-9f97-4e12-801b-ed5c2cc61bba", - "name": "rsa-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "privateKey": [ - "MIIEowIBAAKCAQEAjdo2HZ5ruNnIbkSeAfFYpbPvJw3vtz/VuKJerC4mUXYd7qRMhs3VLJZ3mFyeCuO8W81vkGrFiC9KQnX2lHj2dtA/RWEJw5bpz+JdOFr5pvXg0lQ0sa+hro9afWDygTU4FmLsEi5z98847TbH178RT6n7+JVqZ9jYU9rSpwVTC8E/4yxSuStmhGCcAkZ6dGhHNBdvGUgwxKYj7dYLRJiI+nilIdKuxPzxI/YZxZnXBHDdbNXJgDymTQPut99OnBxeZbH38CJ1MNo3VdV1fzOMGUHe+vn/EOD5E+pXC8PwvJnWU+XHUTFVZeyIXehh3pYLUsq/6bZ1MYsEaFIhznOkwwIDAQABAoIBAHB+64fVyUxRurhoRn737fuLlU/9p2xGfbHtYvNdrhnQeLB3MBGAT10K/1Gfsd6k+Q49AAsiAgGcr2HBt4nL3HohcOwOpvWsS0UIGjHFRFP6jw9+pEN+K9UJ7xObvPZnRFHMpbdNi76tYlINrbMV3h61ihR8OmSc/gKSeZjnihK5OkaNnlqGRaBM/koI+iAxUHuJPnBLBZmD4T8eIfE4S2TvUeVeQogI9Muvnb9tIPJ5XyP9iXWLdRjnek/+wTdxHHZuo06Tc0bMjRaTHiF6K9ntOM2EmQb6bS2J47zgzRLNFE22BWH7RJq659EzElkOn0C0k7dWDTur/3Lpx1+zxJECgYEA8t+J3J+9oGTFmY2VPH05Yr/R/iVDOyIOlO1CmgonOQ3KPhbfNBB3aTAJP20LOZChN4JoWuiZJg4UwzXOeY9DvdDkPO0YLlSjPAIwJNk+xcxFcp5hqMUul2db+cgEY8zp0Wg9kFOq3JmJjK4+1+fgsVnOB+B08ZYI6bZzsUVKzucCgYEAlYTrsxs6fQua0cvZNQPYNZzwF3LVwPBl/ntkdRBE3HGyZeCAhIj7e9pAUusCPsQJaCmW2UEmywD/aIxGrBkojzTKItshM3PN1PYKL8W0Zq+H67uF5KfdvsbabZWHfP/LGCpoKF8Ov7JVPPqGrZ03Z2SheeLZAtNeHN4OB1u9i8UCgYATkS7qN3Rvl67T0DRVy0D0U7/3Wckw2m2SUgsrneXLEvFYTz9sUmdMcjJMidx9pslWT4tYx6SPDFNf5tXbtU8f29SHlBJ+qRL9oq9+SIJmLS7rLRdxIXG/gPRIC3VPFRNBa8SJ/DOn0jbivqcRffz8TN/sgojpbc0KB0kK3ypHwQKBgCKVCcb1R0PgyUA4+9YNO5a647UotFPZxl1jwMpqpuKt0WtKz67X2AK/ah1DidNmmB5lcCRzsztE0c4mk7n+X6kvtoj1UeqKoFLfTV/bRGxzsOZPCxrl0J3tdFvgN+QrbZf7Rvf/dHPWFWzzLO8+66+YUNjWJQdIR/45Rdlh2KdZAoGBAMfF3ir+fe3KdQ6hAf9QyrLxJ5l+GO+IgtxXGbon7eeJBIZHHdMeDy4pC7DMcI214BmIntbyY+xS+gI3oM26EJUVmrZ6tkyIDFsCHm9rcXG9ogvffzQWM1Wqzm27hR/3s+EPWW9AOcIimiFV1UPp/mLjnrCuq58V2aJS/TT14oLe" - ], - "certificate": [ - "MIICmzCCAYMCBgFygL/j4DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZtYXN0ZXIwHhcNMjAwNjA0MTkxMDU4WhcNMzAwNjA0MTkxMjM4WjARMQ8wDQYDVQQDDAZtYXN0ZXIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCN2jYdnmu42chuRJ4B8Vils+8nDe+3P9W4ol6sLiZRdh3upEyGzdUslneYXJ4K47xbzW+QasWIL0pCdfaUePZ20D9FYQnDlunP4l04Wvmm9eDSVDSxr6Guj1p9YPKBNTgWYuwSLnP3zzjtNsfXvxFPqfv4lWpn2NhT2tKnBVMLwT/jLFK5K2aEYJwCRnp0aEc0F28ZSDDEpiPt1gtEmIj6eKUh0q7E/PEj9hnFmdcEcN1s1cmAPKZNA+63306cHF5lsffwInUw2jdV1XV/M4wZQd76+f8Q4PkT6lcLw/C8mdZT5cdRMVVl7Ihd6GHelgtSyr/ptnUxiwRoUiHOc6TDAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIAqydMYxa51kNEyfXyR2kStlglE4LDeLBLHDABeBPE0eN2awoH/mw3kXS4OA/C0e3c7bAwViOzOVERGeUNiBvP5rL1Amuu97nwFcxhkTaJH4ZwCGkxceaIo9LNDpAEesqHLQSdplFXIA4TbEFoKMem4k31KVU7i9/rUesrSRmxLptIOK7LLvRMYiY/t7tdAvoZAtoliuQlFKQywEuxXQrCkcoVEAARABWGt0rsWC2xK0tVxHRIrENwvMp/aUYd17sZ0403aaS9dlvfQ63ExnaHd+++RJtPku8P220Tw27YVmFAwzJgS0aUpEaDsgRNz6OMSyxEg/n7eKK08aU3szwQ=" - ], - "priority": [ - "100" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "3253f9b7-905d-4458-ad8a-8ada5e16d195", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "75bd854e-ab99-46f1-90ed-a8bfc1559558", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth-otp", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "9b0e6cce-62c5-4fb6-a48d-e07c950e38c3", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "1c26fd14-ac06-4dc1-bdd8-8c34c1b41720", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-otp", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "254f7549-51ec-4565-a736-35c07b6e25f0", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "b2413da8-3de9-4bfe-b77e-643fd1964c8f", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Account verification options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "f8392bfb-8dce-4a16-8af1-b2a4d1a0a273", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-otp", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "fb69c297-b26e-44fa-aabd-d7b40eec3cd3", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "de3a41a9-7018-4931-9c4d-d04f9501b2ce", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "6526b0d1-b48e-46c6-bb08-11ebcf458def", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "identity-provider-redirector", - "requirement": "ALTERNATIVE", - "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "92a653ba-8f2d-4283-8354-ca55f9d89181", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-jwt", - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-secret-jwt", - "requirement": "ALTERNATIVE", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-x509", - "requirement": "ALTERNATIVE", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "e365be39-78db-46f0-b2e8-4e7001c2f5d0", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-password", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 30, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "dd61caf5-a40f-48b7-9e8c-a1f3b67041dd", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "7a055643-62e1-4ac1-b126-9a8d6c299635", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "User creation or linking", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "fe8bc7ee-6e8f-436e-8336-c60fcd350843", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "3646f08e-ab70-415b-a701-6ed2e2d214c9", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Authentication Options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "04176530-0972-47ad-83df-19d8534caac2", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "requirement": "REQUIRED", - "priority": 10, - "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "fa0ed569-6746-439e-b07e-89f7ed918c07", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-profile-action", - "requirement": "REQUIRED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-password-action", - "requirement": "REQUIRED", - "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-recaptcha-action", - "requirement": "DISABLED", - "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "03680917-28f3-4ccd-bdf6-4a516f7c0018", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-credential-email", - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-password", - "requirement": "REQUIRED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "requirement": "CONDITIONAL", - "priority": 40, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "19a9d9aa-2d2b-4701-807f-c384ab921c7e", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "534f01f4-45b3-43a0-91d1-238860cc126d", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "65bb9337-9633-4a21-8f6f-1d4129f664ac", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "terms_and_conditions", - "name": "Terms and Conditions", - "providerId": "terms_and_conditions", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": {}, - "keycloakVersion": "10.0.0", - "userManagedAccessAllowed": false -} diff --git a/keycloak/oauth2-proxy-users-0.json b/keycloak/oauth2-proxy-users-0.json deleted file mode 100644 index efcffddb7d8..00000000000 --- a/keycloak/oauth2-proxy-users-0.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "realm": "oauth2-proxy", - "users": [ - { - "id": "3356c0a0-d4d5-4436-9c5a-2299c71c08ec", - "createdTimestamp": 1591297959169, - "username": "admin", - "email": "dataverse@mailinator.com", - "enabled": true, - "totp": false, - "emailVerified": true, - "credentials": [ - { - "id": "a1a06ecd-fdc0-4e67-92cd-2da22d724e32", - "type": "password", - "createdDate": 1591297959315, - "secretData": "{\"value\":\"6rt5zuqHVHopvd0FTFE0CYadXTtzY0mDY2BrqnNQGS51/7DfMJeGgj0roNnGMGvDv30imErNmiSOYl+cL9jiIA==\",\"salt\":\"LI0kqr09JB7J9wvr2Hxzzg==\"}", - "credentialData": "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\"}" - } - ], - "disableableCredentialTypes": [], - "requiredActions": [], - "realmRoles": [ - "offline_access", - "admin", - "uma_authorization" - ], - "clientRoles": { - "account": [ - "view-profile", - "manage-account" - ] - }, - "notBefore": 0, - "groups": [] - } - ] -} From d1120f898c37ea8809c23c09ecf61af441a13d4d Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 21:48:55 +0200 Subject: [PATCH 08/61] restored bearer token config --- docker-compose-dev.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 8c49cc4f502..bc4ede62d95 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -16,6 +16,7 @@ services: ENABLE_RELOAD: "1" SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" + DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" From eea06bd433b5199c453c69e17014ed781dffaad5 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 22:29:33 +0200 Subject: [PATCH 09/61] removed unused import --- src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 969291b0f5a..cc3adc7c8ab 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -6,7 +6,6 @@ import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.Stateless; import jakarta.inject.Inject; -import jakarta.json.JsonObjectBuilder; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.container.ContainerRequestContext; From 1a5cc5d85d3f51624fccd7d30b71955e6103ec26 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Thu, 3 Oct 2024 22:50:53 +0200 Subject: [PATCH 10/61] simplified config --- docker-compose-dev.yml | 5 +---- .../edu/harvard/iq/dataverse/api/OpenIDConfigBean.java | 8 ++++---- .../edu/harvard/iq/dataverse/settings/JvmSettings.java | 8 +------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index bc4ede62d95..c7097076f85 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -23,10 +23,7 @@ services: DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test - DATAVERSE_AUTH_API_OIDC_CLIENT_ID: test - DATAVERSE_AUTH_API_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 - DATAVERSE_AUTH_API_OIDC_PROVIDER_URI: http://keycloak.mydomain.com:8090/realms/test - DATAVERSE_AUTH_API_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token + DATAVERSE_AUTH_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters" # These two oai settings are here to get HarvestingServerIT to pass dataverse_oai_server_maxidentifiers: "2" diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java index dcf9b575073..47784693160 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -8,18 +8,18 @@ @Named("openIdConfigBean") public class OpenIDConfigBean implements java.io.Serializable { public String getProviderURI() { - return JvmSettings.API_OIDC_PROVIDER_URI.lookup(); + return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); } public String getClientId() { - return JvmSettings.API_OIDC_CLIENT_ID.lookup(); + return JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); } public String getClientSecret() { - return JvmSettings.API_OIDC_CLIENT_SECRET.lookup(); + return JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); } public String getRedirectURI() { - return JvmSettings.API_OIDC_REDIRECT_URI.lookup(); + return JvmSettings.OIDC_REDIRECT_URI.lookupOptional().orElse(null); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 282fab901e4..0be49e22ed6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -230,18 +230,12 @@ public enum JvmSettings { OIDC_AUTH_SERVER_URL(SCOPE_OIDC, "auth-server-url"), OIDC_CLIENT_ID(SCOPE_OIDC, "client-id"), OIDC_CLIENT_SECRET(SCOPE_OIDC, "client-secret"), + OIDC_REDIRECT_URI(SCOPE_OIDC, "redirect-uri"), SCOPE_OIDC_PKCE(SCOPE_OIDC, "pkce"), OIDC_PKCE_ENABLED(SCOPE_OIDC_PKCE, "enabled"), OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"), OIDC_PKCE_CACHE_MAXSIZE(SCOPE_OIDC_PKCE, "max-cache-size"), OIDC_PKCE_CACHE_MAXAGE(SCOPE_OIDC_PKCE, "max-cache-age"), - // AUTH: OPEN_ID SETTINGS - SCOPE_AUTH_API(SCOPE_AUTH, "api"), - SCOPE_OPEN_ID(SCOPE_AUTH_API, "oidc"), - API_OIDC_PROVIDER_URI(SCOPE_OPEN_ID, "provider-uri"), - API_OIDC_CLIENT_ID(SCOPE_OPEN_ID, "client-id"), - API_OIDC_CLIENT_SECRET(SCOPE_OPEN_ID, "client-secret"), - API_OIDC_REDIRECT_URI(SCOPE_OPEN_ID, "redirect-uri"), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), From 5c1dc2468ae6315a63edb41de2f4c650c2ebaea4 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 4 Oct 2024 13:17:38 +0200 Subject: [PATCH 11/61] improved implementation with exposed tokens, no unverified emails blocking yet --- .../iq/dataverse/api/AbstractApiBean.java | 4 ++ .../dataverse/api/OpenIDAuthentication.java | 16 +++++--- .../iq/dataverse/api/OpenIDCallback.java | 38 ++++++++++++++++++- .../iq/dataverse/api/OpenIDConfigBean.java | 19 +++++++++- .../iq/dataverse/settings/JvmSettings.java | 1 - 5 files changed, 68 insertions(+), 10 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 3548c7151e1..448b21a9e4d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -46,6 +46,7 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; @@ -245,6 +246,9 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletRequest httpRequest; + @Context + protected HttpServletResponse httpResponse; + /** * For pretty printing (indenting) of JSON output. */ diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java index f9a51c3b2ac..3e5dd2270ee 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java @@ -3,6 +3,7 @@ import java.io.IOException; import fish.payara.security.annotations.OpenIdAuthenticationDefinition; +import fish.payara.security.openid.api.OpenIdConstant; import jakarta.annotation.security.DeclareRoles; import jakarta.ejb.EJB; import jakarta.servlet.ServletException; @@ -13,13 +14,16 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +/** + * OIDC login implementation + */ @WebServlet("/oidc/login") @OpenIdAuthenticationDefinition( - providerURI="#{openIdConfigBean.providerURI}", - clientId="#{openIdConfigBean.clientId}", - clientSecret="#{openIdConfigBean.clientSecret}", - redirectURI="#{openIdConfigBean.redirectURI}", - scope="email" + providerURI = "#{openIdConfigBean.providerURI}", + clientId = "#{openIdConfigBean.clientId}", + clientSecret = "#{openIdConfigBean.clientSecret}", + redirectURI = "#{openIdConfigBean.redirectURI}", + scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE} ) @DeclareRoles("all") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) @@ -29,6 +33,6 @@ public class OpenIDAuthentication extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.getWriter().println("..."); + response.getWriter().println("This content is unreachable as the required role is not assigned to anyone, therefore, this content should never become visible in a browser"); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index cc3adc7c8ab..bf945194ed9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -4,6 +4,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import fish.payara.security.openid.api.OpenIdContext; +import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -21,12 +22,42 @@ public class OpenIDCallback extends AbstractApiBean { @Inject protected AuthenticationServiceBean authSvc; + @EJB + OpenIDConfigBean openIdConfigBean; + + /** + * Callback URL for the OIDC log in. It redirects to either JSF, SPA or API + * after log in according to the target config. + * + * @param crc + * @return + */ @Path("token") @GET public Response token(@Context ContainerRequestContext crc) { - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); + /*final int emailVerified = openIdContext.getAccessToken().getJwtClaims().getIntClaim("email_verified") + .orElse(0); + if (emailVerified == 0) { + openIdContext.logout(httpRequest, httpResponse); + }*/ + switch (openIdConfigBean.getTarget()) { + case "JSF": + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("oauth2/callback.xhtml")).build(); + case "SPA": + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("spa/")).build(); + case "API": + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); + default: + return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("spa/")).build(); + } } + /** + * Retrieve OIDC session and tokens (it is also where API target login redirects to) + * + * @param crc + * @return + */ @Path("session") @GET public Response session(@Context ContainerRequestContext crc) { @@ -37,7 +68,10 @@ public Response session(@Context ContainerRequestContext crc) { return ok( jsonObjectBuilder() .add("user", authUser.toJson()) - .add("session", crc.getCookies().get("JSESSIONID").getValue())); + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + .add("accessToken", openIdContext.getAccessToken().getToken()) + .add("identityToken", openIdContext.getAccessToken().getToken()) + ); } else { return notFound("user with email " + email + " not found"); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java index 47784693160..3acd9310f37 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -1,12 +1,21 @@ package edu.harvard.iq.dataverse.api; import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.ejb.Stateless; +import jakarta.inject.Inject; import jakarta.inject.Named; +import jakarta.servlet.http.HttpServletRequest; @Stateless @Named("openIdConfigBean") public class OpenIDConfigBean implements java.io.Serializable { + + @Inject + HttpServletRequest request; + + private String target = "API"; + public String getProviderURI() { return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); } @@ -20,6 +29,14 @@ public String getClientSecret() { } public String getRedirectURI() { - return JvmSettings.OIDC_REDIRECT_URI.lookupOptional().orElse(null); + return SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/callback/token"; + } + + public String getTarget() { + return this.target; + } + + public String setTarget(String target) { + return this.target = target; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index 0be49e22ed6..d7eea970b8a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -230,7 +230,6 @@ public enum JvmSettings { OIDC_AUTH_SERVER_URL(SCOPE_OIDC, "auth-server-url"), OIDC_CLIENT_ID(SCOPE_OIDC, "client-id"), OIDC_CLIENT_SECRET(SCOPE_OIDC, "client-secret"), - OIDC_REDIRECT_URI(SCOPE_OIDC, "redirect-uri"), SCOPE_OIDC_PKCE(SCOPE_OIDC, "pkce"), OIDC_PKCE_ENABLED(SCOPE_OIDC_PKCE, "enabled"), OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"), From 575d65356f85438ad22d0baed9f8cb87e623f2dd Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 4 Oct 2024 14:09:07 +0200 Subject: [PATCH 12/61] only verified email can be used to log in --- .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../iq/dataverse/api/OpenIDCallback.java | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 448b21a9e4d..407ad333e51 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -332,7 +332,7 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon return (AuthenticatedUser) requestUser; } else { try { - final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final String email = openIdContext.getClaims().getEmail().orElse(null); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); if (authUser != null) { return authUser; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index bf945194ed9..b0c0f3f3705 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -3,6 +3,7 @@ import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import fish.payara.security.openid.api.OpenIdConstant; import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; @@ -35,11 +36,12 @@ public class OpenIDCallback extends AbstractApiBean { @Path("token") @GET public Response token(@Context ContainerRequestContext crc) { - /*final int emailVerified = openIdContext.getAccessToken().getJwtClaims().getIntClaim("email_verified") - .orElse(0); - if (emailVerified == 0) { + final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); + final boolean emailVerified = emailVerifiedObject != null && (Boolean.TRUE.equals(emailVerifiedObject) + || (emailVerifiedObject instanceof String && Boolean.getBoolean((String) emailVerifiedObject))); + if (!emailVerified) { openIdContext.logout(httpRequest, httpResponse); - }*/ + } switch (openIdConfigBean.getTarget()) { case "JSF": return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("oauth2/callback.xhtml")).build(); @@ -53,7 +55,8 @@ public Response token(@Context ContainerRequestContext crc) { } /** - * Retrieve OIDC session and tokens (it is also where API target login redirects to) + * Retrieve OIDC session and tokens (it is also where API target login redirects + * to) * * @param crc * @return @@ -62,7 +65,7 @@ public Response token(@Context ContainerRequestContext crc) { @GET public Response session(@Context ContainerRequestContext crc) { try { - final String email = openIdContext.getAccessToken().getJwtClaims().getStringClaim("email").orElse(null); + final String email = openIdContext.getClaims().getEmail().orElse(null); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); if (authUser != null) { return ok( @@ -70,8 +73,7 @@ public Response session(@Context ContainerRequestContext crc) { .add("user", authUser.toJson()) .add("session", crc.getCookies().get("JSESSIONID").getValue()) .add("accessToken", openIdContext.getAccessToken().getToken()) - .add("identityToken", openIdContext.getAccessToken().getToken()) - ); + .add("identityToken", openIdContext.getIdentityToken().getToken())); } else { return notFound("user with email " + email + " not found"); } From eea3b5ce21541e48da30900dafd8e86a056d4f9e Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 4 Oct 2024 15:12:29 +0200 Subject: [PATCH 13/61] fixed email verified check --- .../iq/dataverse/api/OpenIDAuthentication.java | 4 +++- .../harvard/iq/dataverse/api/OpenIDCallback.java | 14 ++++++++++++-- .../harvard/iq/dataverse/api/OpenIDConfigBean.java | 13 +++++++++++-- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java index 3e5dd2270ee..13c4e176d7e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java @@ -2,6 +2,7 @@ import java.io.IOException; +import fish.payara.security.annotations.LogoutDefinition; import fish.payara.security.annotations.OpenIdAuthenticationDefinition; import fish.payara.security.openid.api.OpenIdConstant; import jakarta.annotation.security.DeclareRoles; @@ -23,7 +24,8 @@ clientId = "#{openIdConfigBean.clientId}", clientSecret = "#{openIdConfigBean.clientSecret}", redirectURI = "#{openIdConfigBean.redirectURI}", - scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE} + scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, + logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}") ) @DeclareRoles("all") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index b0c0f3f3705..9c099c8eff4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -1,6 +1,7 @@ package edu.harvard.iq.dataverse.api; import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; + import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import fish.payara.security.openid.api.OpenIdConstant; @@ -8,6 +9,8 @@ import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.container.ContainerRequestContext; @@ -37,8 +40,15 @@ public class OpenIDCallback extends AbstractApiBean { @GET public Response token(@Context ContainerRequestContext crc) { final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); - final boolean emailVerified = emailVerifiedObject != null && (Boolean.TRUE.equals(emailVerifiedObject) - || (emailVerifiedObject instanceof String && Boolean.getBoolean((String) emailVerifiedObject))); + final boolean emailVerified; + if (emailVerifiedObject instanceof JsonValue) { + final JsonValue v = (JsonValue) emailVerifiedObject; + emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) + || (JsonValue.ValueType.STRING.equals(v.getValueType()) + && Boolean.getBoolean(((JsonString) v).getString())); + } else { + emailVerified = false; + } if (!emailVerified) { openIdContext.logout(httpRequest, httpResponse); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java index 3acd9310f37..46e781b247c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -15,6 +15,7 @@ public class OpenIDConfigBean implements java.io.Serializable { HttpServletRequest request; private String target = "API"; + private String logoutURI = SystemConfig.getDataverseSiteUrlStatic(); public String getProviderURI() { return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); @@ -32,11 +33,19 @@ public String getRedirectURI() { return SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/callback/token"; } + public String getLogoutURI() { + return this.logoutURI; + } + + public void setLogoutURI(String logoutURI) { + this.logoutURI = logoutURI; + } + public String getTarget() { return this.target; } - public String setTarget(String target) { - return this.target = target; + public void setTarget(String target) { + this.target = target; } } From 0703a659f275c3cdbd797c53fe9b368e20d95719 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 4 Oct 2024 18:29:25 +0200 Subject: [PATCH 14/61] oidc JSF log in --- run_dev_env.sh | 3 + .../iq/dataverse/api/AbstractApiBean.java | 3 +- .../iq/dataverse/api/OpenIDCallback.java | 50 +++----- .../oauth2/OAuth2LoginBackingBean.java | 14 ++- .../oauth2/OIDCLoginBackingBean.java | 111 ++++++++++++++++++ 5 files changed, 147 insertions(+), 34 deletions(-) create mode 100755 run_dev_env.sh create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java diff --git a/run_dev_env.sh b/run_dev_env.sh new file mode 100755 index 00000000000..67cfaa20397 --- /dev/null +++ b/run_dev_env.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +mvn -Pct clean package docker:run \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 407ad333e51..4a6085f5a4e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -7,6 +7,7 @@ import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; @@ -332,7 +333,7 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon return (AuthenticatedUser) requestUser; } else { try { - final String email = openIdContext.getClaims().getEmail().orElse(null); + final String email = OIDCLoginBackingBean.getVerifiedEmail(openIdContext); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); if (authUser != null) { return authUser; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 9c099c8eff4..daf1965367e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -2,15 +2,16 @@ import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; +import java.net.URI; + import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import fish.payara.security.openid.api.OpenIdConstant; +import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.container.ContainerRequestContext; @@ -39,22 +40,11 @@ public class OpenIDCallback extends AbstractApiBean { @Path("token") @GET public Response token(@Context ContainerRequestContext crc) { - final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); - final boolean emailVerified; - if (emailVerifiedObject instanceof JsonValue) { - final JsonValue v = (JsonValue) emailVerifiedObject; - emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) - || (JsonValue.ValueType.STRING.equals(v.getValueType()) - && Boolean.getBoolean(((JsonString) v).getString())); - } else { - emailVerified = false; - } - if (!emailVerified) { - openIdContext.logout(httpRequest, httpResponse); - } switch (openIdConfigBean.getTarget()) { case "JSF": - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("oauth2/callback.xhtml")).build(); + return Response + .seeOther(URI.create(SystemConfig.getDataverseSiteUrlStatic() + "/oauth2/callback.xhtml")) + .build(); case "SPA": return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("spa/")).build(); case "API": @@ -74,21 +64,17 @@ public Response token(@Context ContainerRequestContext crc) { @Path("session") @GET public Response session(@Context ContainerRequestContext crc) { - try { - final String email = openIdContext.getClaims().getEmail().orElse(null); - final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); - if (authUser != null) { - return ok( - jsonObjectBuilder() - .add("user", authUser.toJson()) - .add("session", crc.getCookies().get("JSESSIONID").getValue()) - .add("accessToken", openIdContext.getAccessToken().getToken()) - .add("identityToken", openIdContext.getIdentityToken().getToken())); - } else { - return notFound("user with email " + email + " not found"); - } - } catch (final Exception ignore) { - return authenticatedUserRequired(); + final String email = OIDCLoginBackingBean.getVerifiedEmail(openIdContext); + final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + if (authUser != null) { + return ok( + jsonObjectBuilder() + .add("user", authUser.toJson()) + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + .add("accessToken", openIdContext.getAccessToken().getToken()) + .add("identityToken", openIdContext.getIdentityToken().getToken())); + } else { + return notFound("user with email " + email + " not found"); } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 8f3dc07fdea..24d24a10bbd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationProvider; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ClockUtil; @@ -78,6 +79,9 @@ public class OAuth2LoginBackingBean implements Serializable { @Inject @ClockUtil.LocalTime Clock clock; + + @EJB + OIDCLoginBackingBean oidcLoginBackingBean; /** * Generate the OAuth2 Provider URL to be used in the login page link for the provider. @@ -87,6 +91,9 @@ public class OAuth2LoginBackingBean implements Serializable { */ public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); + if (idp instanceof OIDCAuthProvider) { + return oidcLoginBackingBean.getLogInLink(); + } String state = createState(idp, toOption(redirectPage)); return idp.buildAuthzUrl(state, systemConfig.getOAuth2CallbackUrl()); } @@ -97,6 +104,11 @@ public String linkFor(String idpId, String redirectPage) { */ public void exchangeCodeForToken() throws IOException { HttpServletRequest req = Faces.getRequest(); + final String stateParameter = req.getParameter("state"); + if (stateParameter == null || "".equals(stateParameter)) { + oidcLoginBackingBean.setUser(); + return; + } try { Optional oIdp = parseStateFromRequest(req.getParameter("state")); @@ -104,7 +116,7 @@ public void exchangeCodeForToken() throws IOException { if (oIdp.isPresent() && code.isPresent()) { AbstractOAuth2AuthenticationProvider idp = oIdp.get(); - oauthUser = idp.getUserRecord(code.get(), req.getParameter("state"), systemConfig.getOAuth2CallbackUrl()); + oauthUser = idp.getUserRecord(code.get(), stateParameter, systemConfig.getOAuth2CallbackUrl()); // Throw an error if this authentication method is disabled: // (it's not clear if it's possible at all, for somebody to get here with diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java new file mode 100644 index 00000000000..55cc15f9429 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java @@ -0,0 +1,111 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2; + +import java.io.IOException; +import java.io.Serializable; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.omnifaces.util.Faces; + +import edu.harvard.iq.dataverse.DataverseSession; +import edu.harvard.iq.dataverse.UserServiceBean; +import edu.harvard.iq.dataverse.api.OpenIDConfigBean; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.util.SystemConfig; +import fish.payara.security.openid.api.OpenIdConstant; +import fish.payara.security.openid.api.OpenIdContext; +import jakarta.ejb.EJB; +import jakarta.ejb.Stateless; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; + +/** + * Backing bean of the OIDC login process. Used from the login and the + * callback pages. + */ +@Stateless +@Named +public class OIDCLoginBackingBean implements Serializable { + + private static final Logger logger = Logger.getLogger(OAuth2LoginBackingBean.class.getName()); + + @EJB + AuthenticationServiceBean authenticationSvc; + + @EJB + SystemConfig systemConfig; + + @EJB + UserServiceBean userService; + + @Inject + DataverseSession session; + + @Inject + OAuth2FirstLoginPage newAccountPage; + + @Inject + OpenIdContext openIdContext; + + @EJB + OpenIDConfigBean openIdConfigBean; + + /** + * Generate the OIDC log in link. + */ + public String getLogInLink() { + openIdConfigBean.setTarget("JSF"); + final String email = getVerifiedEmail(openIdContext); + if (email != null) { + setUser(); + return SystemConfig.getDataverseSiteUrlStatic(); + } + return SystemConfig.getDataverseSiteUrlStatic() + "/oidc/login"; + } + + /** + * View action for callback.xhtml, the browser redirect target for the OAuth2 + * provider. + * + * @throws IOException + */ + public void setUser() { + final String email = getVerifiedEmail(openIdContext); + AuthenticatedUser dvUser = authenticationSvc.getAuthenticatedUserByEmail(email); + if (dvUser == null) { + logger.log(Level.INFO, "user not found: " + email); + if (!systemConfig.isSignupDisabledForRemoteAuthProvider("oidc-mpconfig")) { + logger.log(Level.INFO, "redirect to first login: " + email); + Faces.redirect("/oauth2/firstLogin.xhtml"); + } + } else { + dvUser = userService.updateLastLogin(dvUser); + session.setUser(dvUser); + Faces.redirect("/"); + } + } + + public static String getVerifiedEmail(final OpenIdContext oidContext) { + if (oidContext.getAccessToken().isExpired()) { + return null; + } + final Object emailVerifiedObject = oidContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); + final boolean emailVerified; + if (emailVerifiedObject instanceof JsonValue) { + final JsonValue v = (JsonValue) emailVerifiedObject; + emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) + || (JsonValue.ValueType.STRING.equals(v.getValueType()) + && Boolean.getBoolean(((JsonString) v).getString())); + } else { + emailVerified = false; + } + if (!emailVerified) { + logger.log(Level.SEVERE, "email not verified: " + oidContext.getClaimsJson().get(OpenIdConstant.EMAIL)); + return null; + } + return oidContext.getClaims().getEmail().orElse(null); + } +} From ff8bb525bdb05701623b9ab1a8f312c55913eb11 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Fri, 4 Oct 2024 21:46:20 +0200 Subject: [PATCH 15/61] bearer token and oidc provider refactoring to use the new payara mechanism --- .../files/root/auth-providers/oidc.json | 2 +- .../source/installation/oidc.rst | 39 +- docker-compose-dev.yml | 2 + docker/compose/demo/compose.yml | 2 + .../iq/dataverse/api/AbstractApiBean.java | 7 +- .../iq/dataverse/api/OpenIDCallback.java | 6 +- .../iq/dataverse/api/OpenIDConfigBean.java | 38 +- .../api/auth/BearerTokenAuthMechanism.java | 102 +++-- .../oauth2/OAuth2LoginBackingBean.java | 2 +- .../oauth2/OIDCLoginBackingBean.java | 56 ++- .../oauth2/oidc/OIDCAuthProvider.java | 377 +++--------------- .../OIDCAuthenticationProviderFactory.java | 8 +- .../iq/dataverse/settings/JvmSettings.java | 8 +- .../META-INF/microprofile-config.properties | 4 +- .../auth/BearerTokenAuthMechanismTest.java | 132 +----- .../OIDCAuthenticationProviderFactoryIT.java | 249 ------------ 16 files changed, 211 insertions(+), 823 deletions(-) delete mode 100644 src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java diff --git a/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json b/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json index 9df38988a25..c950963d776 100644 --- a/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json +++ b/doc/sphinx-guides/source/_static/installation/files/root/auth-providers/oidc.json @@ -3,6 +3,6 @@ "factoryAlias":"oidc", "title":"", "subtitle":"", - "factoryData":"type: oidc | issuer: | clientId: | clientSecret: | pkceEnabled: | pkceMethod: ", + "factoryData":"type: oidc | issuer: | clientId: | clientSecret: ", "enabled":true } \ No newline at end of file diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index d132fd2953d..d6d8bce38b0 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -69,26 +69,6 @@ After adding a provider, the Log In page will by default show the "builtin" prov In contrast to our :doc:`oauth2`, you can use multiple providers by creating distinct configurations enabled by the same technology and without modifying the Dataverse Software code base (standards for the win!). - -.. _oidc-pkce: - -Enabling PKCE Security -^^^^^^^^^^^^^^^^^^^^^^ - -Many providers these days support or even require the usage of `PKCE `_ to safeguard against -some attacks and enable public clients that cannot have a secure secret to still use OpenID Connect (or OAuth2). - -The Dataverse-built OIDC client can be configured to use PKCE and the method to use when creating the code challenge can be specified. -See also `this explanation of the flow `_ -for details on how this works. - -As we are using the `Nimbus SDK `_ as our client -library, we support the standard ``PLAIN`` and ``S256`` (SHA-256) code challenge methods. "SHA-256 method" is the default -as recommend in `RFC7636 `_. If your provider needs some -other method, please open an issue. - -The provisioning sections below contain in the example the parameters you may use to configure PKCE. - Provision a Provider -------------------- @@ -106,9 +86,6 @@ requires fewer extra steps and allows you to keep more configuration in a single Provision via REST API ^^^^^^^^^^^^^^^^^^^^^^ -Note: you may omit the PKCE related settings from ``factoryData`` below if you don't plan on using PKCE - default is -disabled. - Please create a :download:`my-oidc-provider.json <../_static/installation/files/root/auth-providers/oidc.json>` file, replacing every ``<...>`` with your values: .. literalinclude:: /_static/installation/files/root/auth-providers/oidc.json @@ -163,14 +140,6 @@ The following options are available: - The base URL of the OpenID Connect (OIDC) server as explained above. - Y - \- - * - ``dataverse.auth.oidc.pkce.enabled`` - - Set to ``true`` to enable :ref:`PKCE ` in auth flow. - - N - - ``false`` - * - ``dataverse.auth.oidc.pkce.method`` - - Set code challenge method. The default value is the current best practice in the literature. - - N - - ``S256`` * - ``dataverse.auth.oidc.title`` - The UI visible name for this provider in login options. - N @@ -179,12 +148,12 @@ The following options are available: - A subtitle, currently not displayed by the UI. - N - ``OpenID Connect`` - * - ``dataverse.auth.oidc.pkce.max-cache-size`` - - Tune the maximum size of all OIDC providers' verifier cache (the number of outstanding PKCE-enabled auth responses). + * - ``dataverse.auth.oidc.bearer.max-cache-size`` + - Tune the maximum size of all OIDC providers' bearer token cache. - N - 10000 - * - ``dataverse.auth.oidc.pkce.max-cache-age`` - - Tune the maximum age, in seconds, of all OIDC providers' verifier cache entries. Default is 5 minutes, equivalent to lifetime + * - ``dataverse.auth.oidc.bearer.max-cache-age`` + - Tune the maximum age, in seconds, of all OIDC providers' bearer cache entries. Default is 5 minutes, equivalent to lifetime of many OIDC access tokens. - N - 300 \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index c7097076f85..6e1193b1f3d 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -17,6 +17,8 @@ services: SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXSIZE: "10000" + DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXAGE: "300" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 33e7b52004b..adff7379000 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -14,6 +14,8 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse DATAVERSE_FEATURE_API_BEARER_AUTH: "1" + DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXSIZE: "10000" + DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXAGE: "300" DATAVERSE_MAIL_SYSTEM_EMAIL: "Demo Dataverse " DATAVERSE_MAIL_MTA_HOST: "smtp" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 4a6085f5a4e..4b1bdbc02e9 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -47,7 +47,6 @@ import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; @@ -247,8 +246,8 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletRequest httpRequest; - @Context - protected HttpServletResponse httpResponse; + @EJB + OIDCLoginBackingBean oidcLoginBackingBean; /** * For pretty printing (indenting) of JSON output. @@ -333,7 +332,7 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon return (AuthenticatedUser) requestUser; } else { try { - final String email = OIDCLoginBackingBean.getVerifiedEmail(openIdContext); + final String email = oidcLoginBackingBean.getVerifiedEmail(); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); if (authUser != null) { return authUser; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index daf1965367e..22ccf859361 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -30,6 +30,9 @@ public class OpenIDCallback extends AbstractApiBean { @EJB OpenIDConfigBean openIdConfigBean; + @EJB + OIDCLoginBackingBean oidcLoginBackingBean; + /** * Callback URL for the OIDC log in. It redirects to either JSF, SPA or API * after log in according to the target config. @@ -64,9 +67,10 @@ public Response token(@Context ContainerRequestContext crc) { @Path("session") @GET public Response session(@Context ContainerRequestContext crc) { - final String email = OIDCLoginBackingBean.getVerifiedEmail(openIdContext); + final String email = oidcLoginBackingBean.getVerifiedEmail(); final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); if (authUser != null) { + oidcLoginBackingBean.storeBearerToken(); return ok( jsonObjectBuilder() .add("user", authUser.toJson()) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java index 46e781b247c..4ade6eae80d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -3,49 +3,49 @@ import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.SystemConfig; import jakarta.ejb.Stateless; -import jakarta.inject.Inject; import jakarta.inject.Named; -import jakarta.servlet.http.HttpServletRequest; @Stateless @Named("openIdConfigBean") public class OpenIDConfigBean implements java.io.Serializable { - - @Inject - HttpServletRequest request; - private String target = "API"; - private String logoutURI = SystemConfig.getDataverseSiteUrlStatic(); - + private String providerURI = JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); + private String clientId = JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); + private String clientSecret = JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); + public String getProviderURI() { - return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); + return providerURI; } public String getClientId() { - return JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); + return clientId; } public String getClientSecret() { - return JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); + return clientSecret; } public String getRedirectURI() { return SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/callback/token"; } - public String getLogoutURI() { - return this.logoutURI; + public String getTarget() { + return this.target; } - public void setLogoutURI(String logoutURI) { - this.logoutURI = logoutURI; + public void setClientId(final String clientId) { + this.clientId = clientId; } - public String getTarget() { - return this.target; + public void setTarget(final String target) { + this.target = target; } - public void setTarget(String target) { - this.target = target; + public void setProviderURI(final String providerURI) { + this.providerURI = providerURI; + } + + public void setClientSecret(final String clientSecret) { + this.clientSecret = clientSecret; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java index 31f524af3f0..044526a1354 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java @@ -1,38 +1,33 @@ package edu.harvard.iq.dataverse.api.auth; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import java.util.List; +import java.util.Optional; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.settings.FeatureFlags; - import jakarta.inject.Inject; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.HttpHeaders; -import java.io.IOException; -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; public class BearerTokenAuthMechanism implements AuthMechanism { private static final String BEARER_AUTH_SCHEME = "Bearer"; private static final Logger logger = Logger.getLogger(BearerTokenAuthMechanism.class.getCanonicalName()); - + public static final String UNAUTHORIZED_BEARER_TOKEN = "Unauthorized bearer token"; - public static final String INVALID_BEARER_TOKEN = "Could not parse bearer token"; public static final String BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED = "Bearer token detected, no OIDC provider configured"; @Inject protected AuthenticationServiceBean authSvc; @Inject protected UserServiceBean userSvc; - + @Override public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { if (FeatureFlags.API_BEARER_AUTH.enabled()) { @@ -41,22 +36,24 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) if (bearerToken.isEmpty()) { return null; } - - // Validate and verify provided Bearer Token, and retrieve UserRecordIdentifier - // TODO: Get the identifier from an invalidating cache to avoid lookup bursts of the same token. Tokens in the cache should be removed after some (configurable) time. - UserRecordIdentifier userInfo = verifyOidcBearerTokenAndGetUserIdentifier(bearerToken.get()); + + // Validate and verify provided Bearer Token, and retrieve email + String verifiedEmail = getVerifiedEmail(bearerToken.get()); // retrieve Authenticated User from AuthService - AuthenticatedUser authUser = authSvc.lookupUser(userInfo); + AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(verifiedEmail); if (authUser != null) { // track the API usage authUser = userSvc.updateLastApiUseTime(authUser); return authUser; } else { // a valid Token was presented, but we have no associated user account. - logger.log(Level.WARNING, "Bearer token detected, OIDC provider {0} validated Token but no linked UserAccount", userInfo.getUserRepoId()); - // TODO: Instead of returning null, we should throw a meaningful error to the client. - // Probably this will be a wrapped auth error response with an error code and a string describing the problem. + logger.log(Level.WARNING, + "Bearer token detected, OIDC provider found verified email {0} but no linked UserAccount", + verifiedEmail); + // TODO: Instead of returning null, we should throw a meaningful error to the + // client. Probably this will be a wrapped auth error response with an error + // code and a string describing the problem. return null; } } @@ -64,44 +61,33 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } /** - * Verifies the given Bearer token and obtain information about the corresponding user within respective AuthProvider. + * Verifies the given Bearer token and obtain information about the + * corresponding user within respective AuthProvider. * * @param token The string containing the encoded JWT * @return */ - private UserRecordIdentifier verifyOidcBearerTokenAndGetUserIdentifier(String token) throws WrappedAuthErrorResponse { - try { - BearerAccessToken accessToken = BearerAccessToken.parse(token); - // Get list of all authentication providers using Open ID Connect - // @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers. - List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() - .map(providerId -> (OIDCAuthProvider) authSvc.getAuthenticationProvider(providerId)) - .collect(Collectors.toUnmodifiableList()); - // If not OIDC Provider are configured we cannot validate a Token - if(providers.isEmpty()){ - logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); - throw new WrappedAuthErrorResponse(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED); - } + private String getVerifiedEmail(String token) throws WrappedAuthErrorResponse { + // Get list of all authentication providers using Open ID Connect + // @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers. + List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() + .map(providerId -> (OIDCAuthProvider) authSvc.getAuthenticationProvider(providerId)) + .collect(Collectors.toUnmodifiableList()); + // If not OIDC Provider are configured we cannot validate a Token + if (providers.isEmpty()) { + logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); + throw new WrappedAuthErrorResponse(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED); + } - // Iterate over all OIDC providers if multiple. Sadly needed as do not know which provided the Token. - for (OIDCAuthProvider provider : providers) { - try { - // The OIDCAuthProvider need to verify a Bearer Token and equip the client means to identify the corresponding AuthenticatedUser. - Optional userInfo = provider.getUserIdentifier(accessToken); - if(userInfo.isPresent()) { - logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier", provider.getId()); - return userInfo.get(); - } - } catch (IOException e) { - // TODO: Just logging this is not sufficient - if there is an IO error with the one provider - // which would have validated successfully, this is not the users fault. We need to - // take note and refer to that later when occurred. - logger.log(Level.FINE, "Bearer token detected, provider " + provider.getId() + " indicates an invalid Token, skipping", e); - } + // Iterate over all OIDC providers if multiple. Sadly needed as do not know + // which provided the Token. + for (OIDCAuthProvider provider : providers) { + final String email = provider.getVerifiedEmail(token); + if (email != null) { + logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier", + provider.getId()); + return email; } - } catch (ParseException e) { - logger.log(Level.FINE, "Bearer token detected, unable to parse bearer token (invalid Token)", e); - throw new WrappedAuthErrorResponse(INVALID_BEARER_TOKEN); } // No UserInfo returned means we have an invalid access token. @@ -110,12 +96,16 @@ private UserRecordIdentifier verifyOidcBearerTokenAndGetUserIdentifier(String to } /** - * Retrieve the raw, encoded token value from the Authorization Bearer HTTP header as defined in RFC 6750 - * @return An {@link Optional} either empty if not present or the raw token from the header + * Retrieve the raw, encoded token value from the Authorization Bearer HTTP + * header as defined in RFC 6750 + * + * @return An {@link Optional} either empty if not present or the raw token from + * the header */ private Optional getRequestApiKey(ContainerRequestContext containerRequestContext) { String headerParamApiKey = containerRequestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - if (headerParamApiKey != null && headerParamApiKey.toLowerCase().startsWith(BEARER_AUTH_SCHEME.toLowerCase() + " ")) { + if (headerParamApiKey != null + && headerParamApiKey.toLowerCase().startsWith(BEARER_AUTH_SCHEME.toLowerCase() + " ")) { return Optional.of(headerParamApiKey); } else { return Optional.empty(); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 24d24a10bbd..7800cf778c4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -92,7 +92,7 @@ public class OAuth2LoginBackingBean implements Serializable { public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); if (idp instanceof OIDCAuthProvider) { - return oidcLoginBackingBean.getLogInLink(); + return oidcLoginBackingBean.getLogInLink((OIDCAuthProvider) idp); } String state = createState(idp, toOption(redirectPage)); return idp.buildAuthzUrl(state, systemConfig.getOAuth2CallbackUrl()); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java index 55cc15f9429..8fbc4dbe26f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java @@ -2,8 +2,10 @@ import java.io.IOException; import java.io.Serializable; +import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; import org.omnifaces.util.Faces; @@ -11,7 +13,9 @@ import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.api.OpenIDConfigBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.openid.api.OpenIdConstant; import fish.payara.security.openid.api.OpenIdContext; @@ -56,9 +60,9 @@ public class OIDCLoginBackingBean implements Serializable { /** * Generate the OIDC log in link. */ - public String getLogInLink() { - openIdConfigBean.setTarget("JSF"); - final String email = getVerifiedEmail(openIdContext); + public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { + oidcAuthProvider.setConfig(openIdConfigBean); + final String email = getVerifiedEmail(); if (email != null) { setUser(); return SystemConfig.getDataverseSiteUrlStatic(); @@ -73,26 +77,36 @@ public String getLogInLink() { * @throws IOException */ public void setUser() { - final String email = getVerifiedEmail(openIdContext); + final String email = getVerifiedEmail(); AuthenticatedUser dvUser = authenticationSvc.getAuthenticatedUserByEmail(email); if (dvUser == null) { logger.log(Level.INFO, "user not found: " + email); if (!systemConfig.isSignupDisabledForRemoteAuthProvider("oidc-mpconfig")) { logger.log(Level.INFO, "redirect to first login: " + email); + /* + * final OAuth2UserRecord userRecord = OAuth2UserRecord("oidc", + * parsed.userIdInProvider, + * openIdContext.getSubject(), + * OAuth2TokenData.from(openIdContext.getAccessToken()), + * parsed.displayInfo, + * parsed.emails); + * newAccountPage.setNewUser(userRecord); + */ Faces.redirect("/oauth2/firstLogin.xhtml"); } } else { dvUser = userService.updateLastLogin(dvUser); session.setUser(dvUser); + storeBearerToken(); Faces.redirect("/"); } } - public static String getVerifiedEmail(final OpenIdContext oidContext) { - if (oidContext.getAccessToken().isExpired()) { + public String getVerifiedEmail() { + if (openIdContext.getAccessToken().isExpired()) { return null; } - final Object emailVerifiedObject = oidContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); + final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); final boolean emailVerified; if (emailVerifiedObject instanceof JsonValue) { final JsonValue v = (JsonValue) emailVerifiedObject; @@ -103,9 +117,33 @@ public static String getVerifiedEmail(final OpenIdContext oidContext) { emailVerified = false; } if (!emailVerified) { - logger.log(Level.SEVERE, "email not verified: " + oidContext.getClaimsJson().get(OpenIdConstant.EMAIL)); + logger.log(Level.SEVERE, "email not verified: " + openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL)); return null; } - return oidContext.getClaims().getEmail().orElse(null); + return openIdContext.getClaims().getEmail().orElse(null); + } + + public void storeBearerToken() { + if (!FeatureFlags.API_BEARER_AUTH.enabled()) { + return; + } + final String email = getVerifiedEmail(); + if (email == null) { + logger.log(Level.WARNING, "Could not store bearer token, verified email not found"); + } + final String issuerEndpointURL = openIdContext.getClaims().getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER) + .orElse(null); + List providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) + .stream() + .map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId)) + .filter(provider -> issuerEndpointURL.equals(provider.getIssuerEndpointURL())) + .collect(Collectors.toUnmodifiableList()); + // If not OIDC Provider are configured we cannot validate a Token + if (providers.isEmpty()) { + logger.log(Level.WARNING, "OIDC provider not found for URL: " + issuerEndpointURL); + } else { + final String token = openIdContext.getAccessToken().getToken(); + providers.get(0).storeBearerToken(token, email); + } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 5eb2b391eb7..29f86d67ff5 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -1,359 +1,108 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; +import java.time.Duration; +import java.time.temporal.ChronoUnit; + import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.scribejava.core.builder.api.DefaultApi20; -import com.nimbusds.oauth2.sdk.AuthorizationCode; -import com.nimbusds.oauth2.sdk.AuthorizationCodeGrant; -import com.nimbusds.oauth2.sdk.AuthorizationGrant; -import com.nimbusds.oauth2.sdk.ErrorObject; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.ResponseType; -import com.nimbusds.oauth2.sdk.Scope; -import com.nimbusds.oauth2.sdk.TokenRequest; -import com.nimbusds.oauth2.sdk.TokenResponse; -import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; -import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; -import com.nimbusds.oauth2.sdk.auth.Secret; -import com.nimbusds.oauth2.sdk.http.HTTPRequest; -import com.nimbusds.oauth2.sdk.http.HTTPResponse; -import com.nimbusds.oauth2.sdk.id.ClientID; -import com.nimbusds.oauth2.sdk.id.Issuer; -import com.nimbusds.oauth2.sdk.id.State; -import com.nimbusds.oauth2.sdk.pkce.CodeChallengeMethod; -import com.nimbusds.oauth2.sdk.pkce.CodeVerifier; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.AuthenticationRequest; -import com.nimbusds.openid.connect.sdk.Nonce; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; -import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser; -import com.nimbusds.openid.connect.sdk.UserInfoRequest; -import com.nimbusds.openid.connect.sdk.UserInfoResponse; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderConfigurationRequest; -import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; -import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.exceptions.AuthorizationSetupException; + +import edu.harvard.iq.dataverse.api.OpenIDConfigBean; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.BundleUtil; - -import java.io.IOException; -import java.net.URI; -import java.time.Duration; -import java.time.temporal.ChronoUnit; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutionException; -import java.util.logging.Level; -import java.util.logging.Logger; /** - * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} + * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in + * {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} */ public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider { - - private static final Logger logger = Logger.getLogger(OIDCAuthProvider.class.getName()); - protected String id = "oidc"; protected String title = "Open ID Connect"; - protected List scope = Arrays.asList("openid", "email", "profile"); - - final Issuer issuer; - final ClientAuthentication clientAuth; - final OIDCProviderMetadata idpMetadata; - final boolean pkceEnabled; - final CodeChallengeMethod pkceMethod; - + + final String aClientId; + final String aClientSecret; + final String issuerEndpointURL; + /** - * Using PKCE, we create and send a special {@link CodeVerifier}. This contains a secret - * we need again when verifying the response by the provider, thus the cache. - * To be absolutely sure this may not be abused to DDoS us and not let unused verifiers rot, - * use an evicting cache implementation and not a standard map. + * To be absolutely sure this may not be abused to DDoS us and not let unused + * verifiers rot, use an evicting cache implementation and not a standard map. */ - private final Cache verifierCache = Caffeine.newBuilder() - .maximumSize(JvmSettings.OIDC_PKCE_CACHE_MAXSIZE.lookup(Integer.class)) - .expireAfterWrite(Duration.of(JvmSettings.OIDC_PKCE_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS)) - .build(); - - public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL, - boolean pkceEnabled, String pkceMethod) throws AuthorizationSetupException { - this.clientSecret = aClientSecret; // nedded for state creation - this.clientAuth = new ClientSecretBasic(new ClientID(aClientId), new Secret(aClientSecret)); - this.issuer = new Issuer(issuerEndpointURL); - - this.idpMetadata = getMetadata(); - - this.pkceEnabled = pkceEnabled; - this.pkceMethod = CodeChallengeMethod.parse(pkceMethod); + private final Cache verifierCache = Caffeine.newBuilder() + .maximumSize(JvmSettings.OIDC_BEARER_CACHE_MAXSIZE.lookup(Integer.class)) + .expireAfterWrite( + Duration.of(JvmSettings.OIDC_BEARER_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS)) + .build(); + + public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL) { + this.aClientId = aClientId; + this.aClientSecret = aClientSecret; + this.issuerEndpointURL = issuerEndpointURL; } - + + public String getIssuerEndpointURL() { + return this.issuerEndpointURL; + } + + public void setConfig(final OpenIDConfigBean openIdConfigBean) { + openIdConfigBean.setTarget("JSF"); + openIdConfigBean.setClientId(aClientId); + openIdConfigBean.setClientSecret(aClientSecret); + openIdConfigBean.setProviderURI(issuerEndpointURL); + } + /** - * Although this is defined in {@link edu.harvard.iq.dataverse.authorization.AuthenticationProvider}, - * this needs to be present due to bugs in ELResolver (has been modified for Spring). - * TODO: for the future it might be interesting to make this configurable via the provider JSON (it's used for ORCID!) + * Although this is defined in + * {@link edu.harvard.iq.dataverse.authorization.AuthenticationProvider}, + * this needs to be present due to bugs in ELResolver (has been modified for + * Spring). + * TODO: for the future it might be interesting to make this configurable via + * the provider JSON (it's used for ORCID!) + * * @see JBoss Issue 159 - * @see Jakarta EE Bug 43 + * @see Jakarta EE Bug + * 43 * @return false */ @Override public boolean isDisplayIdentifier() { return false; } - - /** - * Setup metadata from OIDC provider during creation of the provider representation - * @return The OIDC provider metadata, if successfull - * @throws IOException when sth. goes wrong with the retrieval - * @throws ParseException when the metadata is not parsable - */ - OIDCProviderMetadata getMetadata() throws AuthorizationSetupException { - try { - var metadata = getMetadata(this.issuer); - // Assert that the provider supports the code flow - if (metadata.getResponseTypes().stream().noneMatch(ResponseType::impliesCodeFlow)) { - throw new AuthorizationSetupException("OIDC provider at "+this.issuer.getValue()+" does not support code flow, disabling."); - } - return metadata; - } catch (IOException ex) { - logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not retrievable: "+ex.getMessage()); - throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not retrievable."); - } catch (ParseException ex) { - logger.severe("OIDC provider metadata at \"+issuerEndpointURL+\" not parsable: "+ex.getMessage()); - throw new AuthorizationSetupException("OIDC provider metadata at "+this.issuer.getValue()+" not parsable."); - } - } - - /** - * Retrieve metadata from OIDC provider (moved here to be mock-/spyable) - * @param issuer The OIDC provider (basically a wrapped URL to endpoint) - * @return The OIDC provider metadata, if successfull - * @throws IOException when sth. goes wrong with the retrieval - * @throws ParseException when the metadata is not parsable - */ - OIDCProviderMetadata getMetadata(Issuer issuer) throws IOException, ParseException { - // Will resolve the OpenID provider metadata automatically - OIDCProviderConfigurationRequest request = new OIDCProviderConfigurationRequest(issuer); - - // Make HTTP request - HTTPRequest httpRequest = request.toHTTPRequest(); - HTTPResponse httpResponse = httpRequest.send(); - - // Parse OpenID provider metadata - return OIDCProviderMetadata.parse(httpResponse.getContentAsJSONObject()); - } - + /** - * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} + * TODO: remove when refactoring package and + * {@link AbstractOAuth2AuthenticationProvider} */ @Override public DefaultApi20 getApiInstance() { throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); } - + /** - * TODO: remove when refactoring package and {@link AbstractOAuth2AuthenticationProvider} + * TODO: remove when refactoring package and + * {@link AbstractOAuth2AuthenticationProvider} */ @Override protected ParsedUserResponse parseUserResponse(String responseBody) { throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); } - - /** - * Create the authz URL for the OIDC provider - * @param state A randomized state, necessary to secure the authorization flow. @see OAuth2LoginBackingBean.createState() - * @param callbackUrl URL where the provider should send the browser after authn in code flow - * @return - */ - @Override - public String buildAuthzUrl(String state, String callbackUrl) { - State stateObject = new State(state); - URI callback = URI.create(callbackUrl); - Nonce nonce = new Nonce(); - CodeVerifier pkceVerifier = pkceEnabled ? new CodeVerifier() : null; - - AuthenticationRequest req = new AuthenticationRequest.Builder(new ResponseType("code"), - Scope.parse(this.scope), - this.clientAuth.getClientID(), - callback) - .endpointURI(idpMetadata.getAuthorizationEndpointURI()) - .state(stateObject) - // Called method is nullsafe - will disable sending a PKCE challenge in case the verifier is not present - .codeChallenge(pkceVerifier, pkceMethod) - .nonce(nonce) - .build(); - - // Cache the PKCE verifier, as we need the secret in it for verification later again, after the client sends us - // the auth code! We use the state to cache the verifier, as the state is unique per authentication event. - if (pkceVerifier != null) { - this.verifierCache.put(state, pkceVerifier); - } - - return req.toURI().toString(); - } - - /** - * Receive user data from OIDC provider after authn/z has been successfull. (Callback view uses this) - * Request a token and access the resource, parse output and return user details. - * @param code The authz code sent from the provider - * @param redirectUrl The redirect URL (some providers require this when fetching the access token, e. g. Google) - * @return A user record containing all user details accessible for us - * @throws IOException Thrown when communication with the provider fails - * @throws OAuth2Exception Thrown when we cannot access the user details for some reason - * @throws InterruptedException Thrown when the requests thread is failing - * @throws ExecutionException Thrown when the requests thread is failing - */ - @Override - public OAuth2UserRecord getUserRecord(String code, String state, String redirectUrl) throws IOException, OAuth2Exception { - // Retrieve the verifier from the cache and clear from the cache. If not found, will be null. - // Will be sent to token endpoint for verification, so if required but missing, will lead to exception. - CodeVerifier verifier = verifierCache.getIfPresent(state); - - // Create grant object - again, this is null-safe for the verifier - AuthorizationGrant codeGrant = new AuthorizationCodeGrant( - new AuthorizationCode(code), URI.create(redirectUrl), verifier); - - // Get Access Token first - Optional accessToken = getAccessToken(codeGrant); - - // Now retrieve User Info - if (accessToken.isPresent()) { - Optional userInfo = getUserInfo(accessToken.get()); - - // Construct our internal user representation - if (userInfo.isPresent()) { - return getUserRecord(userInfo.get()); - } - } - - // this should never happen, as we are throwing exceptions like champs before. - throw new OAuth2Exception(-1, "", "auth.providers.token.failGetUser"); - } - - /** - * Create the OAuth2UserRecord from the OIDC UserInfo. - * TODO: extend to retrieve and insert claims about affiliation and position. - * @param userInfo - * @return the usable user record for processing ing {@link edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2LoginBackingBean} - */ - OAuth2UserRecord getUserRecord(UserInfo userInfo) { - return new OAuth2UserRecord( - this.getId(), - userInfo.getSubject().getValue(), - userInfo.getPreferredUsername(), - null, - new AuthenticatedUserDisplayInfo(userInfo.getGivenName(), userInfo.getFamilyName(), userInfo.getEmailAddress(), "", ""), - null - ); - } - - /** - * Retrieve the Access Token from provider. Encapsulate for testing. - * @param grant - * @return The bearer access token used in code (grant) flow. May be empty if SDK could not cast internally. - */ - Optional getAccessToken(AuthorizationGrant grant) throws IOException, OAuth2Exception { - // Request token - HTTPResponse response = new TokenRequest(this.idpMetadata.getTokenEndpointURI(), - this.clientAuth, - grant, - Scope.parse(this.scope)) - .toHTTPRequest() - .send(); - - // Parse response - try { - TokenResponse tokenRespone = OIDCTokenResponseParser.parse(response); - - // If error --> oauth2 ex - if (! tokenRespone.indicatesSuccess() ) { - ErrorObject error = tokenRespone.toErrorResponse().getErrorObject(); - throw new OAuth2Exception(error.getHTTPStatusCode(), error.getDescription(), "auth.providers.token.failRetrieveToken"); - } - - // Success --> return token - OIDCTokenResponse successResponse = (OIDCTokenResponse)tokenRespone.toSuccessResponse(); - - return Optional.of(successResponse.getOIDCTokens().getBearerAccessToken()); - - } catch (ParseException ex) { - throw new OAuth2Exception(-1, ex.getMessage(), "auth.providers.token.failParseToken"); - } - } - + /** - * Retrieve User Info from provider. Encapsulate for testing. - * @param accessToken The access token to enable reading data from userinfo endpoint + * Trades an access token for an email (if found). + * + * @param accessToken The access token + * @return Returns an email if found */ - Optional getUserInfo(BearerAccessToken accessToken) throws IOException, OAuth2Exception { - // Retrieve data - HTTPResponse response = new UserInfoRequest(this.idpMetadata.getUserInfoEndpointURI(), accessToken) - .toHTTPRequest() - .send(); - - // Parse/Extract - try { - UserInfoResponse infoResponse = UserInfoResponse.parse(response); - - // If error --> oauth2 ex - if (! infoResponse.indicatesSuccess() ) { - ErrorObject error = infoResponse.toErrorResponse().getErrorObject(); - throw new OAuth2Exception(error.getHTTPStatusCode(), - error.getDescription(), - BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); - } - - // Success --> return info - return Optional.of(infoResponse.toSuccessResponse().getUserInfo()); - - } catch (ParseException ex) { - throw new OAuth2Exception(-1, ex.getMessage(), BundleUtil.getStringFromBundle("auth.providers.exception.userinfo", Arrays.asList(this.getTitle()))); - } + public String getVerifiedEmail(String accessToken) { + return this.verifierCache.getIfPresent(accessToken); } /** - * Trades an access token for an {@link UserRecordIdentifier} (if valid). - * - * @apiNote The resulting {@link UserRecordIdentifier} may be used with - * {@link edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean#lookupUser(UserRecordIdentifier)} - * to look up an {@link edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser} from the database. - * @see edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism + * Stores an email in cache for an access token. * - * @param accessToken The token to use when requesting user information from the provider - * @return Returns an {@link UserRecordIdentifier} for a valid access token or an empty {@link Optional}. - * @throws IOException In case communication with the endpoint fails to succeed for an I/O reason + * @param accessToken The access token + * @param email The email */ - public Optional getUserIdentifier(BearerAccessToken accessToken) throws IOException { - OAuth2UserRecord userRecord; - try { - // Try to retrieve with given token (throws if invalid token) - Optional userInfo = getUserInfo(accessToken); - - if (userInfo.isPresent()) { - // Take this detour to avoid code duplication and potentially hard to track conversion errors. - userRecord = getUserRecord(userInfo.get()); - } else { - // This should not happen - an error at the provider side will lead to an exception. - logger.log(Level.WARNING, - "User info retrieval from {0} returned empty optional but expected exception for token {1}.", - List.of(getId(), accessToken).toArray() - ); - return Optional.empty(); - } - } catch (OAuth2Exception e) { - logger.log(Level.FINE, - "Could not retrieve user info with token {0} at provider {1}: {2}", - List.of(accessToken, getId(), e.getMessage()).toArray()); - logger.log(Level.FINER, "Retrieval failed, details as follows: ", e); - return Optional.empty(); - } - - return Optional.of(userRecord.getUserRecordIdentifier()); + public void storeBearerToken(String accessToken, String email) { + this.verifierCache.put(accessToken, email); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java index 3f8c18d0567..89cf1cb986d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactory.java @@ -41,9 +41,7 @@ public AuthenticationProvider buildProvider( AuthenticationProviderRow aRow ) th OIDCAuthProvider oidc = new OIDCAuthProvider( factoryData.get("clientId"), factoryData.get("clientSecret"), - factoryData.get("issuer"), - Boolean.parseBoolean(factoryData.getOrDefault("pkceEnabled", "false")), - factoryData.getOrDefault("pkceMethod", "S256") + factoryData.get("issuer") ); oidc.setId(aRow.getId()); @@ -62,9 +60,7 @@ public static AuthenticationProvider buildFromSettings() throws AuthorizationSet OIDCAuthProvider oidc = new OIDCAuthProvider( JvmSettings.OIDC_CLIENT_ID.lookup(), JvmSettings.OIDC_CLIENT_SECRET.lookup(), - JvmSettings.OIDC_AUTH_SERVER_URL.lookup(), - JvmSettings.OIDC_PKCE_ENABLED.lookupOptional(Boolean.class).orElse(false), - JvmSettings.OIDC_PKCE_METHOD.lookupOptional().orElse("S256") + JvmSettings.OIDC_AUTH_SERVER_URL.lookup() ); oidc.setId("oidc-mpconfig"); diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index d7eea970b8a..df68249235d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -230,11 +230,9 @@ public enum JvmSettings { OIDC_AUTH_SERVER_URL(SCOPE_OIDC, "auth-server-url"), OIDC_CLIENT_ID(SCOPE_OIDC, "client-id"), OIDC_CLIENT_SECRET(SCOPE_OIDC, "client-secret"), - SCOPE_OIDC_PKCE(SCOPE_OIDC, "pkce"), - OIDC_PKCE_ENABLED(SCOPE_OIDC_PKCE, "enabled"), - OIDC_PKCE_METHOD(SCOPE_OIDC_PKCE, "method"), - OIDC_PKCE_CACHE_MAXSIZE(SCOPE_OIDC_PKCE, "max-cache-size"), - OIDC_PKCE_CACHE_MAXAGE(SCOPE_OIDC_PKCE, "max-cache-age"), + SCOPE_OIDC_BEARER(SCOPE_OIDC, "bearer"), + OIDC_BEARER_CACHE_MAXSIZE(SCOPE_OIDC_BEARER, "max-cache-size"), + OIDC_BEARER_CACHE_MAXAGE(SCOPE_OIDC_BEARER, "max-cache-age"), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index b0bc92cf975..13a7b25573e 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -56,5 +56,5 @@ dataverse.oai.server.maxsets=100 #dataverse.oai.server.repositoryname= # AUTHENTICATION -dataverse.auth.oidc.pkce.max-cache-size=10000 -dataverse.auth.oidc.pkce.max-cache-age=300 +dataverse.auth.oidc.bearer.max-cache-size=10000 +dataverse.auth.oidc.bearer.max-cache-age=300 diff --git a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java b/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java index 7e1c23d26f4..52a45909875 100644 --- a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java +++ b/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java @@ -1,30 +1,26 @@ package edu.harvard.iq.dataverse.api.auth; -import com.nimbusds.oauth2.sdk.ParseException; -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import static edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism.BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.api.auth.doubles.BearerTokenKeyContainerRequestTestFake; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.settings.JvmSettings; import edu.harvard.iq.dataverse.util.testing.JvmSetting; import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - import jakarta.ws.rs.container.ContainerRequestContext; -import java.io.IOException; -import java.util.Collections; -import java.util.Optional; - -import static edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism.*; -import static org.junit.jupiter.api.Assertions.*; - @LocalJvmSettings @JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth") class BearerTokenAuthMechanismTest { @@ -48,16 +44,6 @@ void testFindUserFromRequest_no_token() throws WrappedAuthErrorResponse { assertNull(actual); } - @Test - void testFindUserFromRequest_invalid_token() { - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.emptySet()); - - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer "); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(INVALID_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } @Test void testFindUserFromRequest_no_OidcProvider() { Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.emptySet()); @@ -68,100 +54,4 @@ void testFindUserFromRequest_no_OidcProvider() { //then assertEquals(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED, wrappedAuthErrorResponse.getMessage()); } - - @Test - void testFindUserFromRequest_oneProvider_invalidToken_1() throws ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.empty()); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(UNAUTHORIZED_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } - - @Test - void testFindUserFromRequest_oneProvider_invalidToken_2() throws ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenThrow(IOException.class); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(UNAUTHORIZED_BEARER_TOKEN, wrappedAuthErrorResponse.getMessage()); - } - @Test - void testFindUserFromRequest_oneProvider_validToken() throws WrappedAuthErrorResponse, ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - UserRecordIdentifier userinfo = new UserRecordIdentifier(providerID, "KEY"); - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.of(userinfo)); - - // ensures that the AuthenticationServiceBean can retrieve an Authenticated user based on the UserRecordIdentifier - AuthenticatedUser testAuthenticatedUser = new AuthenticatedUser(); - Mockito.when(sut.authSvc.lookupUser(userinfo)).thenReturn(testAuthenticatedUser); - Mockito.when(sut.userSvc.updateLastApiUseTime(testAuthenticatedUser)).thenReturn(testAuthenticatedUser); - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - User actual = sut.findUserFromRequest(testContainerRequest); - - //then - assertEquals(testAuthenticatedUser, actual); - Mockito.verify(sut.userSvc, Mockito.atLeastOnce()).updateLastApiUseTime(testAuthenticatedUser); - - } - @Test - void testFindUserFromRequest_oneProvider_validToken_noAccount() throws WrappedAuthErrorResponse, ParseException, IOException { - OIDCAuthProvider oidcAuthProvider = Mockito.mock(OIDCAuthProvider.class); - String providerID = "OIEDC"; - Mockito.when(oidcAuthProvider.getId()).thenReturn(providerID); - // ensure that a valid OIDCAuthProvider is available within the AuthenticationServiceBean - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.singleton(providerID)); - Mockito.when(sut.authSvc.getAuthenticationProvider(providerID)).thenReturn(oidcAuthProvider); - - // ensure that the OIDCAuthProvider returns a valid UserRecordIdentifier for a given Token - UserRecordIdentifier userinfo = new UserRecordIdentifier(providerID, "KEY"); - BearerAccessToken token = BearerAccessToken.parse("Bearer " + TEST_API_KEY); - Mockito.when(oidcAuthProvider.getUserIdentifier(token)).thenReturn(Optional.of(userinfo)); - - // ensures that the AuthenticationServiceBean can retrieve an Authenticated user based on the UserRecordIdentifier - Mockito.when(sut.authSvc.lookupUser(userinfo)).thenReturn(null); - - - // when - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " + TEST_API_KEY); - User actual = sut.findUserFromRequest(testContainerRequest); - - //then - assertNull(actual); - - } } diff --git a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java b/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java deleted file mode 100644 index ee6823ef98a..00000000000 --- a/src/test/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthenticationProviderFactoryIT.java +++ /dev/null @@ -1,249 +0,0 @@ -package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; - -import com.nimbusds.oauth2.sdk.token.BearerAccessToken; -import com.nimbusds.openid.connect.sdk.claims.UserInfo; -import dasniko.testcontainers.keycloak.KeycloakContainer; -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism; -import edu.harvard.iq.dataverse.api.auth.doubles.BearerTokenKeyContainerRequestTestFake; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2Exception; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.mocks.MockAuthenticatedUser; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.testing.JvmSetting; -import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; -import edu.harvard.iq.dataverse.util.testing.Tags; -import org.htmlunit.FailingHttpStatusCodeException; -import org.htmlunit.WebClient; -import org.htmlunit.WebResponse; -import org.htmlunit.html.HtmlForm; -import org.htmlunit.html.HtmlInput; -import org.htmlunit.html.HtmlPage; -import org.htmlunit.html.HtmlSubmitInput; -import org.junit.jupiter.api.Tag; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.keycloak.OAuth2Constants; -import org.keycloak.admin.client.Keycloak; -import org.keycloak.admin.client.KeycloakBuilder; -import org.mockito.InjectMocks; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import org.testcontainers.junit.jupiter.Container; -import org.testcontainers.junit.jupiter.Testcontainers; - -import java.util.Map; -import java.util.Optional; -import java.util.Set; -import java.util.regex.Pattern; -import java.util.stream.Collectors; - -import static edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactoryIT.clientId; -import static edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthenticationProviderFactoryIT.clientSecret; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assumptions.assumeFalse; -import static org.junit.jupiter.api.Assumptions.assumeTrue; -import static org.mockito.Mockito.when; - -@Tag(Tags.INTEGRATION_TEST) -@Tag(Tags.USES_TESTCONTAINERS) -@Testcontainers(disabledWithoutDocker = true) -@ExtendWith(MockitoExtension.class) -// NOTE: order is important here - Testcontainers must be first, otherwise it's not ready when we call getAuthUrl() -@LocalJvmSettings -@JvmSetting(key = JvmSettings.OIDC_CLIENT_ID, value = clientId) -@JvmSetting(key = JvmSettings.OIDC_CLIENT_SECRET, value = clientSecret) -@JvmSetting(key = JvmSettings.OIDC_AUTH_SERVER_URL, method = "getAuthUrl") -class OIDCAuthenticationProviderFactoryIT { - - static final String clientId = "test"; - static final String clientSecret = "94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8"; - static final String realm = "test"; - static final String realmAdminUser = "admin"; - static final String realmAdminPassword = "admin"; - - static final String adminUser = "kcadmin"; - static final String adminPassword = "kcpassword"; - - // The realm JSON resides in conf/keycloak/test-realm.json and gets avail here using in pom.xml - @Container - static KeycloakContainer keycloakContainer = new KeycloakContainer("quay.io/keycloak/keycloak:22.0") - .withRealmImportFile("keycloak/test-realm.json") - .withAdminUsername(adminUser) - .withAdminPassword(adminPassword); - - // simple method to retrieve the issuer URL, referenced to by @JvmSetting annotations (do no delete) - private static String getAuthUrl() { - return keycloakContainer.getAuthServerUrl() + "/realms/" + realm; - } - - OIDCAuthProvider getProvider() throws Exception { - OIDCAuthProvider oidcAuthProvider = (OIDCAuthProvider) OIDCAuthenticationProviderFactory.buildFromSettings(); - - assumeTrue(oidcAuthProvider.getMetadata().getTokenEndpointURI().toString() - .startsWith(keycloakContainer.getAuthServerUrl())); - - return oidcAuthProvider; - } - - // NOTE: This requires the "direct access grants" for the client to be enabled! - String getBearerTokenViaKeycloakAdminClient() throws Exception { - try (Keycloak keycloak = KeycloakBuilder.builder() - .serverUrl(keycloakContainer.getAuthServerUrl()) - .grantType(OAuth2Constants.PASSWORD) - .realm(realm) - .clientId(clientId) - .clientSecret(clientSecret) - .username(realmAdminUser) - .password(realmAdminPassword) - .scope("openid") - .build()) { - return keycloak.tokenManager().getAccessTokenString(); - } - } - - /** - * This basic test covers configuring an OIDC provider via MPCONFIG and being able to use it. - */ - @Test - void testCreateProvider() throws Exception { - // given - OIDCAuthProvider oidcAuthProvider = getProvider(); - String token = getBearerTokenViaKeycloakAdminClient(); - assumeFalse(token == null); - - Optional info = Optional.empty(); - - // when - try { - info = oidcAuthProvider.getUserInfo(new BearerAccessToken(token)); - } catch (OAuth2Exception e) { - System.out.println(e.getMessageBody()); - } - - //then - assertTrue(info.isPresent()); - assertEquals(realmAdminUser, info.get().getPreferredUsername()); - } - - @Mock - UserServiceBean userService; - @Mock - AuthenticationServiceBean authService; - - @InjectMocks - BearerTokenAuthMechanism bearerTokenAuthMechanism; - - /** - * This test covers using an OIDC provider as authorization party when accessing the Dataverse API with a - * Bearer Token. See {@link BearerTokenAuthMechanism}. It needs to mock the auth services to avoid adding - * more dependencies. - */ - @Test - @JvmSetting(key = JvmSettings.FEATURE_FLAG, varArgs = "api-bearer-auth", value = "true") - void testApiBearerAuth() throws Exception { - assumeFalse(userService == null); - assumeFalse(authService == null); - assumeFalse(bearerTokenAuthMechanism == null); - - // given - // Get the access token from the remote Keycloak in the container - String accessToken = getBearerTokenViaKeycloakAdminClient(); - assumeFalse(accessToken == null); - - OIDCAuthProvider oidcAuthProvider = getProvider(); - // This will also receive the details from the remote Keycloak in the container - UserRecordIdentifier identifier = oidcAuthProvider.getUserIdentifier(new BearerAccessToken(accessToken)).get(); - String token = "Bearer " + accessToken; - BearerTokenKeyContainerRequestTestFake request = new BearerTokenKeyContainerRequestTestFake(token); - AuthenticatedUser user = new MockAuthenticatedUser(); - - // setup mocks (we don't want or need a database here) - when(authService.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Set.of(oidcAuthProvider.getId())); - when(authService.getAuthenticationProvider(oidcAuthProvider.getId())).thenReturn(oidcAuthProvider); - when(authService.lookupUser(identifier)).thenReturn(user); - when(userService.updateLastApiUseTime(user)).thenReturn(user); - - // when (let's do this again, but now with the actual subject under test!) - User lookedUpUser = bearerTokenAuthMechanism.findUserFromRequest(request); - - // then - assertNotNull(lookedUpUser); - assertEquals(user, lookedUpUser); - } - - /** - * This test covers the {@link OIDCAuthProvider#buildAuthzUrl(String, String)} and - * {@link OIDCAuthProvider#getUserRecord(String, String, String)} methods that are used when - * a user authenticates via the JSF UI. It covers enabling PKCE, which is no hard requirement - * by the protocol, but might be required by some provider (as seen with Microsoft Azure AD). - * As we don't have a real browser, we use {@link WebClient} from HtmlUnit as a replacement. - */ - @Test - @JvmSetting(key = JvmSettings.OIDC_PKCE_ENABLED, value = "true") - void testAuthorizationCodeFlowWithPKCE() throws Exception { - // given - String state = "foobar"; - String callbackUrl = "http://localhost:8080/oauth2callback.xhtml"; - - OIDCAuthProvider oidcAuthProvider = getProvider(); - String authzUrl = oidcAuthProvider.buildAuthzUrl(state, callbackUrl); - //System.out.println(authzUrl); - - try (WebClient webClient = new WebClient()) { - webClient.getOptions().setCssEnabled(false); - webClient.getOptions().setJavaScriptEnabled(false); - // We *want* to know about the redirect, as it contains the data we need! - webClient.getOptions().setRedirectEnabled(false); - - HtmlPage loginPage = webClient.getPage(authzUrl); - assumeTrue(loginPage.getTitleText().contains("Sign in to " + realm)); - - HtmlForm form = loginPage.getForms().get(0); - HtmlInput username = form.getInputByName("username"); - HtmlInput password = form.getInputByName("password"); - HtmlSubmitInput submit = form.getInputByName("login"); - - username.type(realmAdminUser); - password.type(realmAdminPassword); - - FailingHttpStatusCodeException exception = assertThrows(FailingHttpStatusCodeException.class, submit::click); - assertEquals(302, exception.getStatusCode()); - - WebResponse response = exception.getResponse(); - assertNotNull(response); - - String callbackLocation = response.getResponseHeaderValue("Location"); - assertTrue(callbackLocation.startsWith(callbackUrl)); - //System.out.println(callbackLocation); - - String queryPart = callbackLocation.trim().split("\\?")[1]; - Map parameters = Pattern.compile("\\s*&\\s*") - .splitAsStream(queryPart) - .map(s -> s.split("=", 2)) - .collect(Collectors.toMap(a -> a[0], a -> a.length > 1 ? a[1]: "")); - //System.out.println(map); - assertTrue(parameters.containsKey("code")); - assertTrue(parameters.containsKey("state")); - - OAuth2UserRecord userRecord = oidcAuthProvider.getUserRecord( - parameters.get("code"), - parameters.get("state"), - callbackUrl - ); - - assertNotNull(userRecord); - assertEquals(realmAdminUser, userRecord.getUsername()); - } catch (OAuth2Exception e) { - System.out.println(e.getMessageBody()); - throw e; - } - } -} \ No newline at end of file From 61703e8e8c742a1c835887ae91110ea46f1e3939 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 00:17:57 +0200 Subject: [PATCH 16/61] fixed log in issues --- .../iq/dataverse/api/OpenIDConfigBean.java | 4 ++ .../oauth2/OIDCLoginBackingBean.java | 46 +++++++++++-------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java index 4ade6eae80d..e6d142997b7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java @@ -29,6 +29,10 @@ public String getRedirectURI() { return SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/callback/token"; } + public String getLogoutURI() { + return SystemConfig.getDataverseSiteUrlStatic(); + } + public String getTarget() { return this.target; } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java index 8fbc4dbe26f..42eb5d33695 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java @@ -103,24 +103,29 @@ public void setUser() { } public String getVerifiedEmail() { - if (openIdContext.getAccessToken().isExpired()) { - return null; - } - final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); - final boolean emailVerified; - if (emailVerifiedObject instanceof JsonValue) { - final JsonValue v = (JsonValue) emailVerifiedObject; - emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) - || (JsonValue.ValueType.STRING.equals(v.getValueType()) - && Boolean.getBoolean(((JsonString) v).getString())); - } else { - emailVerified = false; - } - if (!emailVerified) { - logger.log(Level.SEVERE, "email not verified: " + openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL)); + try { + if (openIdContext.getAccessToken().isExpired()) { + return null; + } + final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); + final boolean emailVerified; + if (emailVerifiedObject instanceof JsonValue) { + final JsonValue v = (JsonValue) emailVerifiedObject; + emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) + || (JsonValue.ValueType.STRING.equals(v.getValueType()) + && Boolean.getBoolean(((JsonString) v).getString())); + } else { + emailVerified = false; + } + if (!emailVerified) { + logger.log(Level.SEVERE, + "email not verified: " + openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL)); + return null; + } + return openIdContext.getClaims().getEmail().orElse(null); + } catch (final Exception ignore) { return null; } - return openIdContext.getClaims().getEmail().orElse(null); } public void storeBearerToken() { @@ -131,14 +136,19 @@ public void storeBearerToken() { if (email == null) { logger.log(Level.WARNING, "Could not store bearer token, verified email not found"); } - final String issuerEndpointURL = openIdContext.getClaims().getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER) + final String issuerEndpointURL = openIdContext.getIdentityToken().getJwtClaims() + .getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER) .orElse(null); + if (issuerEndpointURL == null) { + logger.log(Level.SEVERE, + "Issuer URL (iss) not found in " + openIdContext.getIdentityToken().getJwtClaims().toString()); + return; + } List providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) .stream() .map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId)) .filter(provider -> issuerEndpointURL.equals(provider.getIssuerEndpointURL())) .collect(Collectors.toUnmodifiableList()); - // If not OIDC Provider are configured we cannot validate a Token if (providers.isEmpty()) { logger.log(Level.WARNING, "OIDC provider not found for URL: " + issuerEndpointURL); } else { From 004613ffc5ad5031a1995993ad9c54972936430a Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 02:42:23 +0200 Subject: [PATCH 17/61] redirect to first log in page and user lookup by user record identifier --- conf/keycloak/test-realm.json | 2 +- .../iq/dataverse/api/AbstractApiBean.java | 23 ++--- .../iq/dataverse/api/OpenIDCallback.java | 26 +++-- .../api/auth/BearerTokenAuthMechanism.java | 17 ++-- .../providers/oauth2/OAuth2TokenData.java | 15 +++ .../oauth2/OIDCLoginBackingBean.java | 94 +++++++++++++------ .../oauth2/oidc/OIDCAuthProvider.java | 9 +- 7 files changed, 119 insertions(+), 67 deletions(-) diff --git a/conf/keycloak/test-realm.json b/conf/keycloak/test-realm.json index 5dc0bd6d6ee..efe71cc5d29 100644 --- a/conf/keycloak/test-realm.json +++ b/conf/keycloak/test-realm.json @@ -398,7 +398,7 @@ "emailVerified" : true, "firstName" : "Dataverse", "lastName" : "Admin", - "email" : "dataverse@mailinator.com", + "email" : "dataverse-admin@mailinator.com", "credentials" : [ { "id" : "28f1ece7-26fb-40f1-9174-5ffce7b85c0a", "type" : "password", diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 4b1bdbc02e9..e5cc5f4fa15 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.DataverseRole; import edu.harvard.iq.dataverse.authorization.RoleAssignee; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; @@ -37,10 +38,8 @@ import edu.harvard.iq.dataverse.util.json.JsonUtil; import edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder; import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; -import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; -import jakarta.inject.Inject; import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.persistence.EntityManager; @@ -237,9 +236,6 @@ String getWrappedMessageWhenJson() { @EJB GuestbookResponseServiceBean gbRespSvc; - @Inject - OpenIdContext openIdContext; - @PersistenceContext(unitName = "VDCNet-ejbPU") protected EntityManager em; @@ -331,15 +327,14 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon if (requestUser.isAuthenticated()) { return (AuthenticatedUser) requestUser; } else { - try { - final String email = oidcLoginBackingBean.getVerifiedEmail(); - final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); - if (authUser != null) { - return authUser; - } else { - throw new WrappedResponse(authenticatedUserRequired()); - } - } catch (final Exception ignore) { + final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); + if (userRecordIdentifier == null) { + throw new WrappedResponse(authenticatedUserRequired()); + } + final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); + if (authUser != null) { + return authUser; + } else { throw new WrappedResponse(authenticatedUserRequired()); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java index 22ccf859361..c0c7d7a8286 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java @@ -5,6 +5,7 @@ import java.net.URI; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -67,18 +68,25 @@ public Response token(@Context ContainerRequestContext crc) { @Path("session") @GET public Response session(@Context ContainerRequestContext crc) { - final String email = oidcLoginBackingBean.getVerifiedEmail(); - final AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(email); + final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); + if (userRecordIdentifier == null) { + return notFound("user record identifier not found"); + } + final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); if (authUser != null) { oidcLoginBackingBean.storeBearerToken(); - return ok( - jsonObjectBuilder() - .add("user", authUser.toJson()) - .add("session", crc.getCookies().get("JSESSIONID").getValue()) - .add("accessToken", openIdContext.getAccessToken().getToken()) - .add("identityToken", openIdContext.getIdentityToken().getToken())); + try { + return ok( + jsonObjectBuilder() + .add("user", authUser.toJson()) + .add("session", crc.getCookies().get("JSESSIONID").getValue()) + .add("accessToken", openIdContext.getAccessToken().getToken()) + .add("identityToken", openIdContext.getIdentityToken().getToken())); + } catch (Exception e) { + return badRequest(e.getMessage()); + } } else { - return notFound("user with email " + email + " not found"); + return notFound("user with record identifier " + userRecordIdentifier + " not found"); } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java index 044526a1354..4016fbacb6a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java @@ -8,6 +8,7 @@ import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; @@ -38,10 +39,10 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } // Validate and verify provided Bearer Token, and retrieve email - String verifiedEmail = getVerifiedEmail(bearerToken.get()); + UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(bearerToken.get()); // retrieve Authenticated User from AuthService - AuthenticatedUser authUser = authSvc.getAuthenticatedUserByEmail(verifiedEmail); + AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); if (authUser != null) { // track the API usage authUser = userSvc.updateLastApiUseTime(authUser); @@ -49,8 +50,8 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) } else { // a valid Token was presented, but we have no associated user account. logger.log(Level.WARNING, - "Bearer token detected, OIDC provider found verified email {0} but no linked UserAccount", - verifiedEmail); + "Bearer token detected, OIDC provider found user record identifier {0} but no linked UserAccount", + userRecordIdentifier); // TODO: Instead of returning null, we should throw a meaningful error to the // client. Probably this will be a wrapped auth error response with an error // code and a string describing the problem. @@ -67,7 +68,7 @@ public User findUserFromRequest(ContainerRequestContext containerRequestContext) * @param token The string containing the encoded JWT * @return */ - private String getVerifiedEmail(String token) throws WrappedAuthErrorResponse { + private UserRecordIdentifier getUserRecordIdentifier(String token) throws WrappedAuthErrorResponse { // Get list of all authentication providers using Open ID Connect // @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers. List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() @@ -82,11 +83,11 @@ private String getVerifiedEmail(String token) throws WrappedAuthErrorResponse { // Iterate over all OIDC providers if multiple. Sadly needed as do not know // which provided the Token. for (OIDCAuthProvider provider : providers) { - final String email = provider.getVerifiedEmail(token); - if (email != null) { + final UserRecordIdentifier userRecordIdentifier = provider.getUserRecordIdentifier(token); + if (userRecordIdentifier != null) { logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier", provider.getId()); - return email; + return userRecordIdentifier; } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java index 59f659ff297..8f41f80053a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java @@ -2,6 +2,8 @@ import com.github.scribejava.core.model.OAuth2AccessToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import fish.payara.security.openid.api.OpenIdContext; + import java.io.Serializable; import java.sql.Timestamp; import jakarta.persistence.Column; @@ -83,6 +85,19 @@ public static OAuth2TokenData from( OAuth2AccessToken accessTokenResponse ) { return retVal; } + + public static OAuth2TokenData from(OpenIdContext openIdContext) { + OAuth2TokenData retVal = new OAuth2TokenData(); + retVal.setAccessToken(openIdContext.getAccessToken().getToken()); + //retVal.setRefreshToken(openIdContext.getRefreshToken().isPresent() ? openIdContext.getRefreshToken().get().getToken() : null); + retVal.setRefreshToken("too long > 64 chars"); + retVal.setTokenType(openIdContext.getTokenType()); + if (openIdContext.getExpiresIn().isPresent()) { + retVal.setExpiryDate( new Timestamp(System.currentTimeMillis() + openIdContext.getExpiresIn().get())); + } + retVal.setRawResponse("Not Applicable"); + return retVal; + } public Long getId() { return id; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java index 42eb5d33695..c1851134145 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java @@ -12,11 +12,14 @@ import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.UserServiceBean; import edu.harvard.iq.dataverse.api.OpenIDConfigBean; +import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.settings.FeatureFlags; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.util.SystemConfig; +import fish.payara.security.openid.api.JwtClaims; import fish.payara.security.openid.api.OpenIdConstant; import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; @@ -62,8 +65,8 @@ public class OIDCLoginBackingBean implements Serializable { */ public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { oidcAuthProvider.setConfig(openIdConfigBean); - final String email = getVerifiedEmail(); - if (email != null) { + final UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(); + if (userRecordIdentifier != null) { setUser(); return SystemConfig.getDataverseSiteUrlStatic(); } @@ -77,32 +80,43 @@ public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { * @throws IOException */ public void setUser() { - final String email = getVerifiedEmail(); - AuthenticatedUser dvUser = authenticationSvc.getAuthenticatedUserByEmail(email); - if (dvUser == null) { - logger.log(Level.INFO, "user not found: " + email); - if (!systemConfig.isSignupDisabledForRemoteAuthProvider("oidc-mpconfig")) { - logger.log(Level.INFO, "redirect to first login: " + email); - /* - * final OAuth2UserRecord userRecord = OAuth2UserRecord("oidc", - * parsed.userIdInProvider, - * openIdContext.getSubject(), - * OAuth2TokenData.from(openIdContext.getAccessToken()), - * parsed.displayInfo, - * parsed.emails); - * newAccountPage.setNewUser(userRecord); - */ - Faces.redirect("/oauth2/firstLogin.xhtml"); + try { + final String subject = openIdContext.getSubject(); + final OIDCAuthProvider provider = getProvider(); + final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); + AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier); + if (dvUser == null) { + if (!systemConfig.isSignupDisabledForRemoteAuthProvider(provider.getId())) { + final JwtClaims claims = openIdContext.getIdentityToken().getJwtClaims(); + final String firstName = claims.getStringClaim(OpenIdConstant.GIVEN_NAME).orElse(""); + final String lastName = claims.getStringClaim(OpenIdConstant.FAMILY_NAME).orElse(""); + final String verifiedEmailAddress = getVerifiedEmail(); + final String emailAddress = verifiedEmailAddress == null ? "" : verifiedEmailAddress; + final String affiliation = claims.getStringClaim("affiliation").orElse(""); + final String position = claims.getStringClaim("position").orElse(""); + final OAuth2UserRecord userRecord = new OAuth2UserRecord( + provider.getId(), + subject, + claims.getStringClaim(OpenIdConstant.PREFERRED_USERNAME).orElse(subject), + OAuth2TokenData.from(openIdContext), + new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, position), + List.of(emailAddress)); + logger.log(Level.INFO, "redirect to first login: " + userRecordIdentifier); + newAccountPage.setNewUser(userRecord); + Faces.redirect("/oauth2/firstLogin.xhtml"); + } + } else { + dvUser = userService.updateLastLogin(dvUser); + session.setUser(dvUser); + storeBearerToken(); + Faces.redirect("/"); } - } else { - dvUser = userService.updateLastLogin(dvUser); - session.setUser(dvUser); - storeBearerToken(); - Faces.redirect("/"); + } catch (Exception e) { + logger.log(Level.SEVERE, "Setting user failed: " + e.getMessage()); } } - public String getVerifiedEmail() { + private String getVerifiedEmail() { try { if (openIdContext.getAccessToken().isExpired()) { return null; @@ -132,17 +146,25 @@ public void storeBearerToken() { if (!FeatureFlags.API_BEARER_AUTH.enabled()) { return; } - final String email = getVerifiedEmail(); - if (email == null) { - logger.log(Level.WARNING, "Could not store bearer token, verified email not found"); + try { + final OIDCAuthProvider provider = getProvider(); + final String subject = openIdContext.getSubject(); + final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); + final String token = openIdContext.getAccessToken().getToken(); + provider.storeBearerToken(token, userRecordIdentifier); + } catch (Exception e) { + logger.log(Level.SEVERE, "Storing token failed: " + e.getMessage()); } + } + + private OIDCAuthProvider getProvider() { final String issuerEndpointURL = openIdContext.getIdentityToken().getJwtClaims() .getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER) .orElse(null); if (issuerEndpointURL == null) { logger.log(Level.SEVERE, "Issuer URL (iss) not found in " + openIdContext.getIdentityToken().getJwtClaims().toString()); - return; + return null; } List providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) .stream() @@ -150,10 +172,20 @@ public void storeBearerToken() { .filter(provider -> issuerEndpointURL.equals(provider.getIssuerEndpointURL())) .collect(Collectors.toUnmodifiableList()); if (providers.isEmpty()) { - logger.log(Level.WARNING, "OIDC provider not found for URL: " + issuerEndpointURL); + logger.log(Level.SEVERE, "OIDC provider not found for URL: " + issuerEndpointURL); + return null; } else { - final String token = openIdContext.getAccessToken().getToken(); - providers.get(0).storeBearerToken(token, email); + return providers.get(0); + } + } + + public UserRecordIdentifier getUserRecordIdentifier() { + try { + final String subject = openIdContext.getSubject(); + final OIDCAuthProvider provider = getProvider(); + return new UserRecordIdentifier(provider.getId(), subject); + } catch (final Exception ignore) { + return null; } } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 29f86d67ff5..b79683869f6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -8,6 +8,7 @@ import com.github.scribejava.core.builder.api.DefaultApi20; import edu.harvard.iq.dataverse.api.OpenIDConfigBean; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; @@ -27,7 +28,7 @@ public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider { * To be absolutely sure this may not be abused to DDoS us and not let unused * verifiers rot, use an evicting cache implementation and not a standard map. */ - private final Cache verifierCache = Caffeine.newBuilder() + private final Cache verifierCache = Caffeine.newBuilder() .maximumSize(JvmSettings.OIDC_BEARER_CACHE_MAXSIZE.lookup(Integer.class)) .expireAfterWrite( Duration.of(JvmSettings.OIDC_BEARER_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS)) @@ -92,7 +93,7 @@ protected ParsedUserResponse parseUserResponse(String responseBody) { * @param accessToken The access token * @return Returns an email if found */ - public String getVerifiedEmail(String accessToken) { + public UserRecordIdentifier getUserRecordIdentifier(String accessToken) { return this.verifierCache.getIfPresent(accessToken); } @@ -102,7 +103,7 @@ public String getVerifiedEmail(String accessToken) { * @param accessToken The access token * @param email The email */ - public void storeBearerToken(String accessToken, String email) { - this.verifierCache.put(accessToken, email); + public void storeBearerToken(String accessToken, UserRecordIdentifier userRecordIdentifier) { + this.verifierCache.put(accessToken, userRecordIdentifier); } } From d377505dc7a716e302639fab5042388259463d25 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 18:20:19 +0200 Subject: [PATCH 18/61] Multi-tenancy implementation --- docker-compose-dev.yml | 3 - docker/compose/demo/compose.yml | 2 - .../iq/dataverse/api/AbstractApiBean.java | 2 +- .../{OpenIDCallback.java => OIDCSession.java} | 42 ++--------- .../iq/dataverse/api/OpenIDConfigBean.java | 55 -------------- .../oauth2/OAuth2LoginBackingBean.java | 1 + .../oauth2/oidc/OIDCAuthProvider.java | 37 ++-------- .../{ => oidc}/OIDCLoginBackingBean.java | 74 +++++++++---------- .../oauth2/oidc}/OpenIDAuthentication.java | 11 ++- .../providers/oauth2/oidc/OpenIDCallback.java | 37 ++++++++++ .../oauth2/oidc/OpenIDConfigBean.java | 65 ++++++++++++++++ .../META-INF/microprofile-config.properties | 1 + .../META-INF/microprofile-config.properties | 1 + 13 files changed, 162 insertions(+), 169 deletions(-) rename src/main/java/edu/harvard/iq/dataverse/api/{OpenIDCallback.java => OIDCSession.java} (57%) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java rename src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/{ => oidc}/OIDCLoginBackingBean.java (90%) rename src/main/java/edu/harvard/iq/dataverse/{api => authorization/providers/oauth2/oidc}/OpenIDAuthentication.java (76%) create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 6e1193b1f3d..402a95c0e16 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -17,15 +17,12 @@ services: SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" DATAVERSE_FEATURE_API_BEARER_AUTH: "1" - DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXSIZE: "10000" - DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXAGE: "300" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" DATAVERSE_AUTH_OIDC_CLIENT_ID: test DATAVERSE_AUTH_OIDC_CLIENT_SECRET: 94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8 DATAVERSE_AUTH_OIDC_AUTH_SERVER_URL: http://keycloak.mydomain.com:8090/realms/test - DATAVERSE_AUTH_OIDC_REDIRECT_URI: http://localhost:8080/api/v1/callback/token DATAVERSE_SPI_EXPORTERS_DIRECTORY: "/dv/exporters" # These two oai settings are here to get HarvestingServerIT to pass dataverse_oai_server_maxidentifiers: "2" diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index adff7379000..33e7b52004b 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -14,8 +14,6 @@ services: DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse DATAVERSE_FEATURE_API_BEARER_AUTH: "1" - DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXSIZE: "10000" - DATAVERSE_AUTH_OIDC_BEARER_CACHE_MAXAGE: "300" DATAVERSE_MAIL_SYSTEM_EMAIL: "Demo Dataverse " DATAVERSE_MAIL_MTA_HOST: "smtp" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index e5cc5f4fa15..214273cebf7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -8,7 +8,7 @@ import edu.harvard.iq.dataverse.authorization.RoleAssignee; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.groups.GroupServiceBean; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.users.User; import edu.harvard.iq.dataverse.confirmemail.ConfirmEmailServiceBean; diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java similarity index 57% rename from src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java rename to src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java index c0c7d7a8286..35bf2f217e3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java @@ -2,13 +2,10 @@ import static edu.harvard.iq.dataverse.util.json.NullSafeJsonBuilder.jsonObjectBuilder; -import java.net.URI; - import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OIDCLoginBackingBean; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.EJB; import jakarta.ejb.Stateless; @@ -20,47 +17,19 @@ import jakarta.ws.rs.core.Response; @Stateless -@Path("callback") -public class OpenIDCallback extends AbstractApiBean { +@Path("oidc") +public class OIDCSession extends AbstractApiBean { @Inject OpenIdContext openIdContext; @Inject protected AuthenticationServiceBean authSvc; - @EJB - OpenIDConfigBean openIdConfigBean; - @EJB OIDCLoginBackingBean oidcLoginBackingBean; /** - * Callback URL for the OIDC log in. It redirects to either JSF, SPA or API - * after log in according to the target config. - * - * @param crc - * @return - */ - @Path("token") - @GET - public Response token(@Context ContainerRequestContext crc) { - switch (openIdConfigBean.getTarget()) { - case "JSF": - return Response - .seeOther(URI.create(SystemConfig.getDataverseSiteUrlStatic() + "/oauth2/callback.xhtml")) - .build(); - case "SPA": - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("spa/")).build(); - case "API": - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("callback/session")).build(); - default: - return Response.seeOther(crc.getUriInfo().getBaseUri().resolve("spa/")).build(); - } - } - - /** - * Retrieve OIDC session and tokens (it is also where API target login redirects - * to) + * Retrieve OIDC session and tokens * * @param crc * @return @@ -80,8 +49,7 @@ public Response session(@Context ContainerRequestContext crc) { jsonObjectBuilder() .add("user", authUser.toJson()) .add("session", crc.getCookies().get("JSESSIONID").getValue()) - .add("accessToken", openIdContext.getAccessToken().getToken()) - .add("identityToken", openIdContext.getIdentityToken().getToken())); + .add("accessToken", openIdContext.getAccessToken().getToken())); } catch (Exception e) { return badRequest(e.getMessage()); } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java deleted file mode 100644 index e6d142997b7..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDConfigBean.java +++ /dev/null @@ -1,55 +0,0 @@ -package edu.harvard.iq.dataverse.api; - -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.SystemConfig; -import jakarta.ejb.Stateless; -import jakarta.inject.Named; - -@Stateless -@Named("openIdConfigBean") -public class OpenIDConfigBean implements java.io.Serializable { - private String target = "API"; - private String providerURI = JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); - private String clientId = JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); - private String clientSecret = JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); - - public String getProviderURI() { - return providerURI; - } - - public String getClientId() { - return clientId; - } - - public String getClientSecret() { - return clientSecret; - } - - public String getRedirectURI() { - return SystemConfig.getDataverseSiteUrlStatic() + "/api/v1/callback/token"; - } - - public String getLogoutURI() { - return SystemConfig.getDataverseSiteUrlStatic(); - } - - public String getTarget() { - return this.target; - } - - public void setClientId(final String clientId) { - this.clientId = clientId; - } - - public void setTarget(final String target) { - this.target = target; - } - - public void setProviderURI(final String providerURI) { - this.providerURI = providerURI; - } - - public void setClientSecret(final String clientSecret) { - this.clientSecret = clientSecret; - } -} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 7800cf778c4..38404af52eb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -6,6 +6,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.util.BundleUtil; import edu.harvard.iq.dataverse.util.ClockUtil; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index b79683869f6..35e45f5231f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -7,7 +7,6 @@ import com.github.benmanes.caffeine.cache.Caffeine; import com.github.scribejava.core.builder.api.DefaultApi20; -import edu.harvard.iq.dataverse.api.OpenIDConfigBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; import edu.harvard.iq.dataverse.settings.JvmSettings; @@ -40,48 +39,28 @@ public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEnd this.issuerEndpointURL = issuerEndpointURL; } - public String getIssuerEndpointURL() { - return this.issuerEndpointURL; + public String getClientId() { + return aClientId; } - public void setConfig(final OpenIDConfigBean openIdConfigBean) { - openIdConfigBean.setTarget("JSF"); - openIdConfigBean.setClientId(aClientId); - openIdConfigBean.setClientSecret(aClientSecret); - openIdConfigBean.setProviderURI(issuerEndpointURL); + public String getClientSecret() { + return aClientSecret; + } + + public String getIssuerEndpointURL() { + return this.issuerEndpointURL; } - /** - * Although this is defined in - * {@link edu.harvard.iq.dataverse.authorization.AuthenticationProvider}, - * this needs to be present due to bugs in ELResolver (has been modified for - * Spring). - * TODO: for the future it might be interesting to make this configurable via - * the provider JSON (it's used for ORCID!) - * - * @see JBoss Issue 159 - * @see Jakarta EE Bug - * 43 - * @return false - */ @Override public boolean isDisplayIdentifier() { return false; } - /** - * TODO: remove when refactoring package and - * {@link AbstractOAuth2AuthenticationProvider} - */ @Override public DefaultApi20 getApiInstance() { throw new UnsupportedOperationException("OIDC provider cannot provide a ScribeJava API instance object"); } - /** - * TODO: remove when refactoring package and - * {@link AbstractOAuth2AuthenticationProvider} - */ @Override protected ParsedUserResponse parseUserResponse(String responseBody) { throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java similarity index 90% rename from src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java rename to src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index c1851134145..a57e7b132a7 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -1,4 +1,4 @@ -package edu.harvard.iq.dataverse.authorization.providers.oauth2; +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; import java.io.IOException; import java.io.Serializable; @@ -11,11 +11,12 @@ import edu.harvard.iq.dataverse.DataverseSession; import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.api.OpenIDConfigBean; import edu.harvard.iq.dataverse.authorization.AuthenticatedUserDisplayInfo; import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2FirstLoginPage; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; +import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2TokenData; import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.util.SystemConfig; @@ -36,8 +37,7 @@ @Stateless @Named public class OIDCLoginBackingBean implements Serializable { - - private static final Logger logger = Logger.getLogger(OAuth2LoginBackingBean.class.getName()); + private static final Logger logger = Logger.getLogger(OIDCLoginBackingBean.class.getName()); @EJB AuthenticationServiceBean authenticationSvc; @@ -57,20 +57,16 @@ public class OIDCLoginBackingBean implements Serializable { @Inject OpenIdContext openIdContext; - @EJB - OpenIDConfigBean openIdConfigBean; - /** * Generate the OIDC log in link. */ public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { - oidcAuthProvider.setConfig(openIdConfigBean); final UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(); if (userRecordIdentifier != null) { setUser(); return SystemConfig.getDataverseSiteUrlStatic(); } - return SystemConfig.getDataverseSiteUrlStatic() + "/oidc/login"; + return "/oidc/login?target=JSF&oidcp="+oidcAuthProvider.getId(); } /** @@ -87,7 +83,7 @@ public void setUser() { AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier); if (dvUser == null) { if (!systemConfig.isSignupDisabledForRemoteAuthProvider(provider.getId())) { - final JwtClaims claims = openIdContext.getIdentityToken().getJwtClaims(); + final JwtClaims claims = openIdContext.getAccessToken().getJwtClaims(); final String firstName = claims.getStringClaim(OpenIdConstant.GIVEN_NAME).orElse(""); final String lastName = claims.getStringClaim(OpenIdConstant.FAMILY_NAME).orElse(""); final String verifiedEmailAddress = getVerifiedEmail(); @@ -116,6 +112,31 @@ public void setUser() { } } + public UserRecordIdentifier getUserRecordIdentifier() { + try { + final String subject = openIdContext.getSubject(); + final OIDCAuthProvider provider = getProvider(); + return new UserRecordIdentifier(provider.getId(), subject); + } catch (final Exception ignore) { + return null; + } + } + + public void storeBearerToken() { + if (!FeatureFlags.API_BEARER_AUTH.enabled()) { + return; + } + try { + final OIDCAuthProvider provider = getProvider(); + final String subject = openIdContext.getSubject(); + final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); + final String token = openIdContext.getAccessToken().getToken(); + provider.storeBearerToken(token, userRecordIdentifier); + } catch (Exception e) { + logger.log(Level.SEVERE, "Storing token failed: " + e.getMessage()); + } + } + private String getVerifiedEmail() { try { if (openIdContext.getAccessToken().isExpired()) { @@ -125,7 +146,7 @@ private String getVerifiedEmail() { final boolean emailVerified; if (emailVerifiedObject instanceof JsonValue) { final JsonValue v = (JsonValue) emailVerifiedObject; - emailVerified = JsonValue.TRUE.equals(emailVerifiedObject) + emailVerified = JsonValue.TRUE.equals(v) || (JsonValue.ValueType.STRING.equals(v.getValueType()) && Boolean.getBoolean(((JsonString) v).getString())); } else { @@ -142,28 +163,13 @@ private String getVerifiedEmail() { } } - public void storeBearerToken() { - if (!FeatureFlags.API_BEARER_AUTH.enabled()) { - return; - } - try { - final OIDCAuthProvider provider = getProvider(); - final String subject = openIdContext.getSubject(); - final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); - final String token = openIdContext.getAccessToken().getToken(); - provider.storeBearerToken(token, userRecordIdentifier); - } catch (Exception e) { - logger.log(Level.SEVERE, "Storing token failed: " + e.getMessage()); - } - } - private OIDCAuthProvider getProvider() { - final String issuerEndpointURL = openIdContext.getIdentityToken().getJwtClaims() + final String issuerEndpointURL = openIdContext.getAccessToken().getJwtClaims() .getStringClaim(OpenIdConstant.ISSUER_IDENTIFIER) .orElse(null); if (issuerEndpointURL == null) { logger.log(Level.SEVERE, - "Issuer URL (iss) not found in " + openIdContext.getIdentityToken().getJwtClaims().toString()); + "Issuer URL (iss) not found in " + openIdContext.getAccessToken().getJwtClaims().toString()); return null; } List providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) @@ -178,14 +184,4 @@ private OIDCAuthProvider getProvider() { return providers.get(0); } } - - public UserRecordIdentifier getUserRecordIdentifier() { - try { - final String subject = openIdContext.getSubject(); - final OIDCAuthProvider provider = getProvider(); - return new UserRecordIdentifier(provider.getId(), subject); - } catch (final Exception ignore) { - return null; - } - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java similarity index 76% rename from src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java rename to src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 13c4e176d7e..eda9778f2dd 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -1,7 +1,8 @@ -package edu.harvard.iq.dataverse.api; +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; import java.io.IOException; +import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.annotations.LogoutDefinition; import fish.payara.security.annotations.OpenIdAuthenticationDefinition; import fish.payara.security.openid.api.OpenIdConstant; @@ -24,8 +25,9 @@ clientId = "#{openIdConfigBean.clientId}", clientSecret = "#{openIdConfigBean.clientSecret}", redirectURI = "#{openIdConfigBean.redirectURI}", + logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}"), scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, - logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}") + tokenAutoRefresh = true ) @DeclareRoles("all") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) @@ -35,6 +37,9 @@ public class OpenIDAuthentication extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { - response.getWriter().println("This content is unreachable as the required role is not assigned to anyone, therefore, this content should never become visible in a browser"); + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + final String target = request.getParameter("target"); + final String redirect = "SPA".equals(target) ? baseURL + "/spa/" : baseURL; + response.sendRedirect(redirect); } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java new file mode 100644 index 00000000000..cb956c36e87 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java @@ -0,0 +1,37 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import java.io.IOException; + +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.servlet.ServletException; +import jakarta.servlet.annotation.WebServlet; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@WebServlet("/oidc/callback/*") +public class OpenIDCallback extends HttpServlet { + @Override + protected void doGet(HttpServletRequest request, HttpServletResponse response) + throws ServletException, IOException { + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + final String target = request.getPathInfo(); + final String redirect; + switch (target) { + case "/JSF": + redirect = baseURL + "/oauth2/callback.xhtml"; + break; + case "/SPA": + redirect = baseURL + "/spa/"; + break; + case "/API": + redirect = baseURL + "/api/v1/oidc/session"; + break; + + default: + redirect = baseURL + "/oauth2/callback.xhtml"; + break; + } + response.sendRedirect(redirect); + } +} diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java new file mode 100644 index 00000000000..c1352e5ee28 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java @@ -0,0 +1,65 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.settings.JvmSettings; +import edu.harvard.iq.dataverse.util.SystemConfig; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.servlet.http.HttpServletRequest; + +@Named("openIdConfigBean") +public class OpenIDConfigBean implements java.io.Serializable { + @Inject + HttpServletRequest request; + + @Inject + AuthenticationServiceBean authenticationSvc; + + public String getProviderURI() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp == "") { + return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getIssuerEndpointURL(); + } catch (Exception e) { + return ""; + } + } + + public String getClientId() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp == "") { + return JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getClientId(); + } catch (Exception e) { + return ""; + } + } + + public String getClientSecret() { + final String oidcp = request.getParameter("oidcp"); + if (oidcp == null || oidcp == "") { + return JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); + } + try { + return ((OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(oidcp)).getClientSecret(); + } catch (Exception e) { + return ""; + } + } + + public String getRedirectURI() { + String target = request.getParameter("target"); + target = target == null || target == "" ? "API" : target; + return SystemConfig.getDataverseSiteUrlStatic() + "/oidc/callback/" + target; + } + + public String getLogoutURI() { + final String target = request.getParameter("target"); + final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); + return "SPA".equals(target) ? baseURL + "/spa/" : baseURL; + } +} diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 13a7b25573e..2baa94215da 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -58,3 +58,4 @@ dataverse.oai.server.maxsets=100 # AUTHENTICATION dataverse.auth.oidc.bearer.max-cache-size=10000 dataverse.auth.oidc.bearer.max-cache-age=300 +payara.security.openid.sessionScopedConfiguration=true diff --git a/src/test/resources/META-INF/microprofile-config.properties b/src/test/resources/META-INF/microprofile-config.properties index 113a098a1fe..378e65a120a 100644 --- a/src/test/resources/META-INF/microprofile-config.properties +++ b/src/test/resources/META-INF/microprofile-config.properties @@ -16,3 +16,4 @@ test.filesDir=/tmp/dataverse dataverse.files.directory=${test.filesDir} dataverse.files.uploads=${test.filesDir}/uploads dataverse.files.docroot=${test.filesDir}/docroot +payara.security.openid.sessionScopedConfiguration=true From 895e054563f4e7dab8c91d536831ceac961f6d7f Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 21:06:57 +0200 Subject: [PATCH 19/61] bearer token mechanism for OIDC --- .../source/installation/config.rst | 4 +- .../source/installation/oidc.rst | 10 -- docker-compose-dev.yml | 1 - docker/compose/demo/compose.yml | 1 - .../iq/dataverse/api/AbstractApiBean.java | 18 +++ .../harvard/iq/dataverse/api/OIDCSession.java | 6 - .../api/auth/BearerTokenAuthMechanism.java | 115 ------------------ .../api/auth/CompoundAuthMechanism.java | 4 +- .../oauth2/oidc/BearerTokenMechanism.java | 53 ++++++++ .../oauth2/oidc/OIDCAuthProvider.java | 37 ------ .../oauth2/oidc/OIDCLoginBackingBean.java | 17 --- .../iq/dataverse/settings/FeatureFlags.java | 6 - .../iq/dataverse/settings/JvmSettings.java | 3 - .../auth/BearerTokenAuthMechanismTest.java | 57 --------- 14 files changed, 74 insertions(+), 258 deletions(-) delete mode 100644 src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java create mode 100644 src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java delete mode 100644 src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java diff --git a/doc/sphinx-guides/source/installation/config.rst b/doc/sphinx-guides/source/installation/config.rst index e98ed8f5189..f08e490e854 100644 --- a/doc/sphinx-guides/source/installation/config.rst +++ b/doc/sphinx-guides/source/installation/config.rst @@ -754,12 +754,10 @@ As for the "Remote only" authentication mode, it means that: Bearer Token Authentication --------------------------- -Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens. This is an experimental feature hidden behind a feature flag. +Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens. .. _RFC 6750: https://tools.ietf.org/html/rfc6750 -To enable bearer tokens, you must install and configure Keycloak (for now, see :ref:`oidc-dev` in the Developer Guide) and enable ``api-bearer-auth`` under :ref:`feature-flags`. - You can test that bearer tokens are working by following the example under :ref:`bearer-tokens` in the API Guide. .. _smtp-config: diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index d6d8bce38b0..b0fea4557b9 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -147,13 +147,3 @@ The following options are available: * - ``dataverse.auth.oidc.subtitle`` - A subtitle, currently not displayed by the UI. - N - - ``OpenID Connect`` - * - ``dataverse.auth.oidc.bearer.max-cache-size`` - - Tune the maximum size of all OIDC providers' bearer token cache. - - N - - 10000 - * - ``dataverse.auth.oidc.bearer.max-cache-age`` - - Tune the maximum age, in seconds, of all OIDC providers' bearer cache entries. Default is 5 minutes, equivalent to lifetime - of many OIDC access tokens. - - N - - 300 \ No newline at end of file diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml index 402a95c0e16..a25b56e929e 100644 --- a/docker-compose-dev.yml +++ b/docker-compose-dev.yml @@ -16,7 +16,6 @@ services: ENABLE_RELOAD: "1" SKIP_DEPLOY: "${SKIP_DEPLOY}" DATAVERSE_JSF_REFRESH_PERIOD: "1" - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "dataverse@localhost" DATAVERSE_MAIL_MTA_HOST: "smtp" DATAVERSE_AUTH_OIDC_ENABLED: "1" diff --git a/docker/compose/demo/compose.yml b/docker/compose/demo/compose.yml index 33e7b52004b..17609fefab1 100644 --- a/docker/compose/demo/compose.yml +++ b/docker/compose/demo/compose.yml @@ -13,7 +13,6 @@ services: DATAVERSE_DB_HOST: postgres DATAVERSE_DB_PASSWORD: secret DATAVERSE_DB_USER: dataverse - DATAVERSE_FEATURE_API_BEARER_AUTH: "1" DATAVERSE_MAIL_SYSTEM_EMAIL: "Demo Dataverse " DATAVERSE_MAIL_MTA_HOST: "smtp" JVM_ARGS: -Ddataverse.files.storage-driver-id=file1 diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 214273cebf7..f0479ec7103 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -40,12 +40,16 @@ import edu.harvard.iq.dataverse.validation.PasswordValidatorServiceBean; import jakarta.ejb.EJB; import jakarta.ejb.EJBException; +import jakarta.inject.Inject; import jakarta.json.*; import jakarta.json.JsonValue.ValueType; import jakarta.persistence.EntityManager; import jakarta.persistence.NoResultException; import jakarta.persistence.PersistenceContext; +import jakarta.security.enterprise.AuthenticationStatus; +import jakarta.security.enterprise.SecurityContext; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotNull; import jakarta.ws.rs.container.ContainerRequestContext; import jakarta.ws.rs.core.Context; @@ -242,9 +246,15 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletRequest httpRequest; + @Context + protected HttpServletResponse httpResponse; + @EJB OIDCLoginBackingBean oidcLoginBackingBean; + @Inject + private SecurityContext securityContext; + /** * For pretty printing (indenting) of JSON output. */ @@ -329,6 +339,14 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon } else { final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); if (userRecordIdentifier == null) { + AuthenticationStatus status = securityContext.authenticate(httpRequest, httpResponse, null); + if (AuthenticationStatus.SUCCESS.equals(status)) { + try { + return (AuthenticatedUser) httpRequest.getAttribute(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER); + } catch (Exception e) { + throw new WrappedResponse(authenticatedUserRequired()); + } + } throw new WrappedResponse(authenticatedUserRequired()); } final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java index 35bf2f217e3..1c0fa67b7ec 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java @@ -4,10 +4,8 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCLoginBackingBean; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import fish.payara.security.openid.api.OpenIdContext; -import jakarta.ejb.EJB; import jakarta.ejb.Stateless; import jakarta.inject.Inject; import jakarta.ws.rs.GET; @@ -25,9 +23,6 @@ public class OIDCSession extends AbstractApiBean { @Inject protected AuthenticationServiceBean authSvc; - @EJB - OIDCLoginBackingBean oidcLoginBackingBean; - /** * Retrieve OIDC session and tokens * @@ -43,7 +38,6 @@ public Response session(@Context ContainerRequestContext crc) { } final AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); if (authUser != null) { - oidcLoginBackingBean.storeBearerToken(); try { return ok( jsonObjectBuilder() diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java deleted file mode 100644 index 4016fbacb6a..00000000000 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanism.java +++ /dev/null @@ -1,115 +0,0 @@ -package edu.harvard.iq.dataverse.api.auth; - -import java.util.List; -import java.util.Optional; -import java.util.logging.Level; -import java.util.logging.Logger; -import java.util.stream.Collectors; - -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; -import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.FeatureFlags; -import jakarta.inject.Inject; -import jakarta.ws.rs.container.ContainerRequestContext; -import jakarta.ws.rs.core.HttpHeaders; - -public class BearerTokenAuthMechanism implements AuthMechanism { - private static final String BEARER_AUTH_SCHEME = "Bearer"; - private static final Logger logger = Logger.getLogger(BearerTokenAuthMechanism.class.getCanonicalName()); - - public static final String UNAUTHORIZED_BEARER_TOKEN = "Unauthorized bearer token"; - public static final String BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED = "Bearer token detected, no OIDC provider configured"; - - @Inject - protected AuthenticationServiceBean authSvc; - @Inject - protected UserServiceBean userSvc; - - @Override - public User findUserFromRequest(ContainerRequestContext containerRequestContext) throws WrappedAuthErrorResponse { - if (FeatureFlags.API_BEARER_AUTH.enabled()) { - Optional bearerToken = getRequestApiKey(containerRequestContext); - // No Bearer Token present, hence no user can be authenticated - if (bearerToken.isEmpty()) { - return null; - } - - // Validate and verify provided Bearer Token, and retrieve email - UserRecordIdentifier userRecordIdentifier = getUserRecordIdentifier(bearerToken.get()); - - // retrieve Authenticated User from AuthService - AuthenticatedUser authUser = authSvc.lookupUser(userRecordIdentifier); - if (authUser != null) { - // track the API usage - authUser = userSvc.updateLastApiUseTime(authUser); - return authUser; - } else { - // a valid Token was presented, but we have no associated user account. - logger.log(Level.WARNING, - "Bearer token detected, OIDC provider found user record identifier {0} but no linked UserAccount", - userRecordIdentifier); - // TODO: Instead of returning null, we should throw a meaningful error to the - // client. Probably this will be a wrapped auth error response with an error - // code and a string describing the problem. - return null; - } - } - return null; - } - - /** - * Verifies the given Bearer token and obtain information about the - * corresponding user within respective AuthProvider. - * - * @param token The string containing the encoded JWT - * @return - */ - private UserRecordIdentifier getUserRecordIdentifier(String token) throws WrappedAuthErrorResponse { - // Get list of all authentication providers using Open ID Connect - // @TASK: Limited to OIDCAuthProviders, could be widened to OAuth2Providers. - List providers = authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() - .map(providerId -> (OIDCAuthProvider) authSvc.getAuthenticationProvider(providerId)) - .collect(Collectors.toUnmodifiableList()); - // If not OIDC Provider are configured we cannot validate a Token - if (providers.isEmpty()) { - logger.log(Level.WARNING, "Bearer token detected, no OIDC provider configured"); - throw new WrappedAuthErrorResponse(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED); - } - - // Iterate over all OIDC providers if multiple. Sadly needed as do not know - // which provided the Token. - for (OIDCAuthProvider provider : providers) { - final UserRecordIdentifier userRecordIdentifier = provider.getUserRecordIdentifier(token); - if (userRecordIdentifier != null) { - logger.log(Level.FINE, "Bearer token detected, provider {0} confirmed validity and provided identifier", - provider.getId()); - return userRecordIdentifier; - } - } - - // No UserInfo returned means we have an invalid access token. - logger.log(Level.FINE, "Bearer token detected, yet no configured OIDC provider validated it."); - throw new WrappedAuthErrorResponse(UNAUTHORIZED_BEARER_TOKEN); - } - - /** - * Retrieve the raw, encoded token value from the Authorization Bearer HTTP - * header as defined in RFC 6750 - * - * @return An {@link Optional} either empty if not present or the raw token from - * the header - */ - private Optional getRequestApiKey(ContainerRequestContext containerRequestContext) { - String headerParamApiKey = containerRequestContext.getHeaderString(HttpHeaders.AUTHORIZATION); - if (headerParamApiKey != null - && headerParamApiKey.toLowerCase().startsWith(BEARER_AUTH_SCHEME.toLowerCase() + " ")) { - return Optional.of(headerParamApiKey); - } else { - return Optional.empty(); - } - } -} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java b/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java index 801e2752b9e..d3eb0c6898b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/auth/CompoundAuthMechanism.java @@ -19,9 +19,9 @@ public class CompoundAuthMechanism implements AuthMechanism { private final List authMechanisms = new ArrayList<>(); @Inject - public CompoundAuthMechanism(ApiKeyAuthMechanism apiKeyAuthMechanism, WorkflowKeyAuthMechanism workflowKeyAuthMechanism, SignedUrlAuthMechanism signedUrlAuthMechanism, SessionCookieAuthMechanism sessionCookieAuthMechanism, BearerTokenAuthMechanism bearerTokenAuthMechanism) { + public CompoundAuthMechanism(ApiKeyAuthMechanism apiKeyAuthMechanism, WorkflowKeyAuthMechanism workflowKeyAuthMechanism, SignedUrlAuthMechanism signedUrlAuthMechanism, SessionCookieAuthMechanism sessionCookieAuthMechanism) { // Auth mechanisms should be ordered by priority here - add(apiKeyAuthMechanism, workflowKeyAuthMechanism, signedUrlAuthMechanism, sessionCookieAuthMechanism,bearerTokenAuthMechanism); + add(apiKeyAuthMechanism, workflowKeyAuthMechanism, signedUrlAuthMechanism, sessionCookieAuthMechanism); } public CompoundAuthMechanism(AuthMechanism... authMechanisms) { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java new file mode 100644 index 00000000000..9fbbaaa24f5 --- /dev/null +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java @@ -0,0 +1,53 @@ +package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; + +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import edu.harvard.iq.dataverse.api.ApiConstants; +import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; +import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; +import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import fish.payara.security.openid.api.AccessTokenCallerPrincipal; +import fish.payara.security.openid.api.BearerGroupsIdentityStore; +import fish.payara.security.openid.api.JwtClaims; +import jakarta.annotation.security.DeclareRoles; +import jakarta.ejb.EJB; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.servlet.http.HttpServletRequest; + +@ApplicationScoped +@DeclareRoles({ "all" }) +public class BearerTokenMechanism extends BearerGroupsIdentityStore { + private static final Logger logger = Logger.getLogger(OIDCLoginBackingBean.class.getName()); + + @Inject + HttpServletRequest request; + + @EJB + AuthenticationServiceBean authenticationSvc; + + @Override + protected Set getCallerGroups(AccessTokenCallerPrincipal callerPrincipal) { + try { + final JwtClaims claims = callerPrincipal.getClaims(); + final String issuer = claims.getIssuer().get(); + final String subject = claims.getSubject().get(); + final OIDCAuthProvider provider = authenticationSvc + .getAuthenticationProviderIdsOfType(OIDCAuthProvider.class).stream() + .map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId)) + .filter(providerCandidate -> issuer.equals(providerCandidate.getIssuerEndpointURL())) + .collect(Collectors.toUnmodifiableList()).get(0); + final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); + final AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier); + request.setAttribute(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER, dvUser); + logger.log(Level.FINE, "user found: " + dvUser.toJson()); + return Set.of("all"); + } catch (Exception e) { + logger.log(Level.SEVERE, "Getting user from bearer token failed: " + e.getMessage()); + } + return Set.of(); + } +} \ No newline at end of file diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java index 35e45f5231f..9c3923faeaf 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCAuthProvider.java @@ -1,15 +1,8 @@ package edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc; -import java.time.Duration; -import java.time.temporal.ChronoUnit; - -import com.github.benmanes.caffeine.cache.Cache; -import com.github.benmanes.caffeine.cache.Caffeine; import com.github.scribejava.core.builder.api.DefaultApi20; -import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.providers.oauth2.AbstractOAuth2AuthenticationProvider; -import edu.harvard.iq.dataverse.settings.JvmSettings; /** * TODO: this should not EXTEND, but IMPLEMENT the contract to be used in @@ -23,16 +16,6 @@ public class OIDCAuthProvider extends AbstractOAuth2AuthenticationProvider { final String aClientSecret; final String issuerEndpointURL; - /** - * To be absolutely sure this may not be abused to DDoS us and not let unused - * verifiers rot, use an evicting cache implementation and not a standard map. - */ - private final Cache verifierCache = Caffeine.newBuilder() - .maximumSize(JvmSettings.OIDC_BEARER_CACHE_MAXSIZE.lookup(Integer.class)) - .expireAfterWrite( - Duration.of(JvmSettings.OIDC_BEARER_CACHE_MAXAGE.lookup(Integer.class), ChronoUnit.SECONDS)) - .build(); - public OIDCAuthProvider(String aClientId, String aClientSecret, String issuerEndpointURL) { this.aClientId = aClientId; this.aClientSecret = aClientSecret; @@ -65,24 +48,4 @@ public DefaultApi20 getApiInstance() { protected ParsedUserResponse parseUserResponse(String responseBody) { throw new UnsupportedOperationException("OIDC provider uses the SDK to parse the response."); } - - /** - * Trades an access token for an email (if found). - * - * @param accessToken The access token - * @return Returns an email if found - */ - public UserRecordIdentifier getUserRecordIdentifier(String accessToken) { - return this.verifierCache.getIfPresent(accessToken); - } - - /** - * Stores an email in cache for an access token. - * - * @param accessToken The access token - * @param email The email - */ - public void storeBearerToken(String accessToken, UserRecordIdentifier userRecordIdentifier) { - this.verifierCache.put(accessToken, userRecordIdentifier); - } } diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index a57e7b132a7..ef7d07a7f4f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -17,7 +17,6 @@ import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2FirstLoginPage; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2TokenData; -import edu.harvard.iq.dataverse.settings.FeatureFlags; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.openid.api.JwtClaims; @@ -104,7 +103,6 @@ public void setUser() { } else { dvUser = userService.updateLastLogin(dvUser); session.setUser(dvUser); - storeBearerToken(); Faces.redirect("/"); } } catch (Exception e) { @@ -122,21 +120,6 @@ public UserRecordIdentifier getUserRecordIdentifier() { } } - public void storeBearerToken() { - if (!FeatureFlags.API_BEARER_AUTH.enabled()) { - return; - } - try { - final OIDCAuthProvider provider = getProvider(); - final String subject = openIdContext.getSubject(); - final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); - final String token = openIdContext.getAccessToken().getToken(); - provider.storeBearerToken(token, userRecordIdentifier); - } catch (Exception e) { - logger.log(Level.SEVERE, "Storing token failed: " + e.getMessage()); - } - } - private String getVerifiedEmail() { try { if (openIdContext.getAccessToken().isExpired()) { diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java index 33e828e619d..ac4fe15a701 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/FeatureFlags.java @@ -30,12 +30,6 @@ public enum FeatureFlags { * @since Dataverse 5.14 */ API_SESSION_AUTH("api-session-auth"), - /** - * Enables API authentication via Bearer Token. - * @apiNote Raise flag by setting "dataverse.feature.api-bearer-auth" - * @since Dataverse @TODO: - */ - API_BEARER_AUTH("api-bearer-auth"), /** * For published (public) objects, don't use a join when searching Solr. * Experimental! Requires a reindex with the following feature flag enabled, diff --git a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java index df68249235d..ecda4992df6 100644 --- a/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java +++ b/src/main/java/edu/harvard/iq/dataverse/settings/JvmSettings.java @@ -230,9 +230,6 @@ public enum JvmSettings { OIDC_AUTH_SERVER_URL(SCOPE_OIDC, "auth-server-url"), OIDC_CLIENT_ID(SCOPE_OIDC, "client-id"), OIDC_CLIENT_SECRET(SCOPE_OIDC, "client-secret"), - SCOPE_OIDC_BEARER(SCOPE_OIDC, "bearer"), - OIDC_BEARER_CACHE_MAXSIZE(SCOPE_OIDC_BEARER, "max-cache-size"), - OIDC_BEARER_CACHE_MAXAGE(SCOPE_OIDC_BEARER, "max-cache-age"), // UI SETTINGS SCOPE_UI(PREFIX, "ui"), diff --git a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java b/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java deleted file mode 100644 index 52a45909875..00000000000 --- a/src/test/java/edu/harvard/iq/dataverse/api/auth/BearerTokenAuthMechanismTest.java +++ /dev/null @@ -1,57 +0,0 @@ -package edu.harvard.iq.dataverse.api.auth; - -import static edu.harvard.iq.dataverse.api.auth.BearerTokenAuthMechanism.BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; - -import java.util.Collections; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -import edu.harvard.iq.dataverse.UserServiceBean; -import edu.harvard.iq.dataverse.api.auth.doubles.BearerTokenKeyContainerRequestTestFake; -import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OIDCAuthProvider; -import edu.harvard.iq.dataverse.authorization.users.User; -import edu.harvard.iq.dataverse.settings.JvmSettings; -import edu.harvard.iq.dataverse.util.testing.JvmSetting; -import edu.harvard.iq.dataverse.util.testing.LocalJvmSettings; -import jakarta.ws.rs.container.ContainerRequestContext; - -@LocalJvmSettings -@JvmSetting(key = JvmSettings.FEATURE_FLAG, value = "true", varArgs = "api-bearer-auth") -class BearerTokenAuthMechanismTest { - - private static final String TEST_API_KEY = "test-api-key"; - - private BearerTokenAuthMechanism sut; - - @BeforeEach - public void setUp() { - sut = new BearerTokenAuthMechanism(); - sut.authSvc = Mockito.mock(AuthenticationServiceBean.class); - sut.userSvc = Mockito.mock(UserServiceBean.class); - } - - @Test - void testFindUserFromRequest_no_token() throws WrappedAuthErrorResponse { - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake(null); - User actual = sut.findUserFromRequest(testContainerRequest); - - assertNull(actual); - } - - @Test - void testFindUserFromRequest_no_OidcProvider() { - Mockito.when(sut.authSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class)).thenReturn(Collections.emptySet()); - - ContainerRequestContext testContainerRequest = new BearerTokenKeyContainerRequestTestFake("Bearer " +TEST_API_KEY); - WrappedAuthErrorResponse wrappedAuthErrorResponse = assertThrows(WrappedAuthErrorResponse.class, () -> sut.findUserFromRequest(testContainerRequest)); - - //then - assertEquals(BEARER_TOKEN_DETECTED_NO_OIDC_PROVIDER_CONFIGURED, wrappedAuthErrorResponse.getMessage()); - } -} From ac43c9442115cc5ab6765a671a5c5800e5241c69 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 23:29:00 +0200 Subject: [PATCH 20/61] OIDC token is no loger stored in DB after first log in --- .../providers/oauth2/OAuth2TokenData.java | 14 -------------- .../oauth2/oidc/OIDCLoginBackingBean.java | 5 ++--- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java index 8f41f80053a..7fa3355df06 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2TokenData.java @@ -2,7 +2,6 @@ import com.github.scribejava.core.model.OAuth2AccessToken; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; -import fish.payara.security.openid.api.OpenIdContext; import java.io.Serializable; import java.sql.Timestamp; @@ -85,19 +84,6 @@ public static OAuth2TokenData from( OAuth2AccessToken accessTokenResponse ) { return retVal; } - - public static OAuth2TokenData from(OpenIdContext openIdContext) { - OAuth2TokenData retVal = new OAuth2TokenData(); - retVal.setAccessToken(openIdContext.getAccessToken().getToken()); - //retVal.setRefreshToken(openIdContext.getRefreshToken().isPresent() ? openIdContext.getRefreshToken().get().getToken() : null); - retVal.setRefreshToken("too long > 64 chars"); - retVal.setTokenType(openIdContext.getTokenType()); - if (openIdContext.getExpiresIn().isPresent()) { - retVal.setExpiryDate( new Timestamp(System.currentTimeMillis() + openIdContext.getExpiresIn().get())); - } - retVal.setRawResponse("Not Applicable"); - return retVal; - } public Long getId() { return id; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index ef7d07a7f4f..116ae11c37e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -16,7 +16,6 @@ import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2FirstLoginPage; import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2UserRecord; -import edu.harvard.iq.dataverse.authorization.providers.oauth2.OAuth2TokenData; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.util.SystemConfig; import fish.payara.security.openid.api.JwtClaims; @@ -65,7 +64,7 @@ public String getLogInLink(final OIDCAuthProvider oidcAuthProvider) { setUser(); return SystemConfig.getDataverseSiteUrlStatic(); } - return "/oidc/login?target=JSF&oidcp="+oidcAuthProvider.getId(); + return "/oidc/login?target=JSF&oidcp=" + oidcAuthProvider.getId(); } /** @@ -93,7 +92,7 @@ public void setUser() { provider.getId(), subject, claims.getStringClaim(OpenIdConstant.PREFERRED_USERNAME).orElse(subject), - OAuth2TokenData.from(openIdContext), + null, new AuthenticatedUserDisplayInfo(firstName, lastName, emailAddress, affiliation, position), List.of(emailAddress)); logger.log(Level.INFO, "redirect to first login: " + userRecordIdentifier); From baa02ea08ec5e6cccc0962a688249b592a6dc36c Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Sat, 5 Oct 2024 23:51:26 +0200 Subject: [PATCH 21/61] python bearer token example --- bearer-token-example/get_session.py | 28 +++++++++++++++++++++++++++ bearer-token-example/requirements.txt | 2 ++ bearer-token-example/run.sh | 7 +++++++ 3 files changed, 37 insertions(+) create mode 100644 bearer-token-example/get_session.py create mode 100644 bearer-token-example/requirements.txt create mode 100755 bearer-token-example/run.sh diff --git a/bearer-token-example/get_session.py b/bearer-token-example/get_session.py new file mode 100644 index 00000000000..60f49b7a8e7 --- /dev/null +++ b/bearer-token-example/get_session.py @@ -0,0 +1,28 @@ +import contextlib +import selenium.webdriver as webdriver +import selenium.webdriver.support.ui as ui +import re +import json +import requests + +with contextlib.closing(webdriver.Firefox()) as driver: + driver.get("http://localhost:8080/oidc/login?target=API") + wait = ui.WebDriverWait(driver, 100) # timeout after 100 seconds + wait.until(lambda driver: "accessToken" in driver.page_source) + driver.get("view-source:http://localhost:8080/api/v1/oidc/session") + result = wait.until( + lambda driver: ( + driver.page_source if "accessToken" in driver.page_source else False + ) + ) + m = re.search("
(.+?)
", result) + if m: + found = m.group(1) + session = json.loads(found) + + token = session["data"]["accessToken"] + endpoint = "http://localhost:8080/api/v1/users/:me" + headers = {"Authorization": "Bearer " + token} + + print() + print(requests.get(endpoint, headers=headers).json()) diff --git a/bearer-token-example/requirements.txt b/bearer-token-example/requirements.txt new file mode 100644 index 00000000000..fdd089ed0b9 --- /dev/null +++ b/bearer-token-example/requirements.txt @@ -0,0 +1,2 @@ +selenium +requests \ No newline at end of file diff --git a/bearer-token-example/run.sh b/bearer-token-example/run.sh new file mode 100755 index 00000000000..864555c8f94 --- /dev/null +++ b/bearer-token-example/run.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +python3 -m venv run_env +source run_env/bin/activate +python3 -m pip install -r requirements.txt +python3 get_session.py +rm -rf run_env \ No newline at end of file From 1db8c105164d50a1f8fc71a1a64e57fa2a0f1d5b Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 12:27:51 +0200 Subject: [PATCH 22/61] added documentation --- bearer-token-example/get_session.py | 2 +- doc/sphinx-guides/source/api/auth.rst | 10 ++--- doc/sphinx-guides/source/api/native-api.rst | 42 +++++++++++++++++++ .../source/installation/oidc.rst | 19 +++++++++ .../oauth2/oidc/OpenIDAuthentication.java | 3 +- .../META-INF/microprofile-config.properties | 2 - 6 files changed, 69 insertions(+), 9 deletions(-) diff --git a/bearer-token-example/get_session.py b/bearer-token-example/get_session.py index 60f49b7a8e7..bccbc3df9f5 100644 --- a/bearer-token-example/get_session.py +++ b/bearer-token-example/get_session.py @@ -6,7 +6,7 @@ import requests with contextlib.closing(webdriver.Firefox()) as driver: - driver.get("http://localhost:8080/oidc/login?target=API") + driver.get("http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig") wait = ui.WebDriverWait(driver, 100) # timeout after 100 seconds wait.until(lambda driver: "accessToken" in driver.page_source) driver.get("view-source:http://localhost:8080/api/v1/oidc/session") diff --git a/doc/sphinx-guides/source/api/auth.rst b/doc/sphinx-guides/source/api/auth.rst index eae3bd3c969..163564580f4 100644 --- a/doc/sphinx-guides/source/api/auth.rst +++ b/doc/sphinx-guides/source/api/auth.rst @@ -69,17 +69,17 @@ You can reset your API Token from your account page in your Dataverse installati Bearer Tokens ------------- -Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens if your installation has been set up to use them (see :ref:`bearer-token-auth` in the Installation Guide). +Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to API tokens if your installation has been set up to use OpenID Connect log in (see :ref:`oidc-log-in` in the Installation Guide). .. _RFC 6750: https://tools.ietf.org/html/rfc6750 -To test if bearer tokens are working, you can try something like the following (using the :ref:`User Information` API endpoint), substituting in parameters for your installation and user. +To test if bearer tokens are working, you can use a Python script that prompts you to log in to the Keycloak in a new browser window using selenium. For example, you can run the script inside the `bearer-token-example` that illustrates this: .. code-block:: bash + cd bearer-token-example + ./run.sh - export TOKEN=`curl -s -X POST --location "http://keycloak.mydomain.com:8090/realms/test/protocol/openid-connect/token" -H "Content-Type: application/x-www-form-urlencoded" -d "username=user&password=user&grant_type=password&client_id=test&client_secret=94XHrfNRwXsjqTqApRrwWmhDLDHpIYV8" | jq '.access_token' -r | tr -d "\n"` - - curl -H "Authorization: Bearer $TOKEN" http://localhost:8080/api/users/:me +This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own script that lets users performed some custom scripted tasks. Signed URLs ----------- diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index f8b8620f121..ae003d04464 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -6430,3 +6430,45 @@ Parameters: ``per_page`` Number of results returned per page. +.. _oidc-session: +Session +------- + +The Session API is used to get the information on the current OIDC session (after being successfully authenticated using the OpenID Connect :ref:`oidc-log-in`). +You can be either redirected to that endpoint using the `API` log in flow as illustrated in the :ref:`bearer-tokens` example, or going to this endpoint directly, +after logging-in in your browser. The returned JSON looks like this: + +.. code-block:: json + + { + "status": "OK", + "data": { + "user": { + "id": 3, + "userIdentifier": "aUser", + "lastName": "User", + "firstName": "Dataverse", + "email": "dataverse-user@mailinator.com", + "isSuperuser": false, + "createdTime": "2024-10-07 08:26:29.453", + "lastLoginTime": "2024-10-07 08:26:29.453", + "deactivated": false, + "mutedEmails": [], + "mutedNotifications": [] + }, + "session": "6164900bf35e7f576a92e4f771cc", + "accessToken": "eyJhbGc...7VvYOMYxreH-Uo3RpaA" + } + } + +You can then use the retrieved `session` and `accessToken` for subsequent calls to the API or the session endpoint, as illustrated in the following curl examples: + +.. code-block:: bash + + export BEARER_TOKEN=eyJhbGc...7VvYOMYxreH-Uo3RpaA + export SESSION=6164900bf35e7f576a92e4f771cc + export SERVER_URL=https://demo.dataverse.org + + curl -H "Authorization: Bearer $BEARER_TOKEN" "$SERVER_URL/api/oidc/session" + + curl -v --cookie "JSESSIONID=$SESSION" "$SERVER_URL/api/oidc/session" diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index b0fea4557b9..bda063ec361 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -147,3 +147,22 @@ The following options are available: * - ``dataverse.auth.oidc.subtitle`` - A subtitle, currently not displayed by the UI. - N + +.. _oidc-log-in: +Choosing provisioned providers at log in +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In the JSF frontend, you can select the provider you wish to log in with at the logging in time. However, you can also use the log +in link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the +browser, it will prompt you with the Keycloak and redirect you to the API endpoint for retrieving the session :ref:`oidc-session`): +`http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig`_. + +The `oidc` parameter is the provisioned provider ID you wish to use and is configured in the previous steps. For example, +`oidc-mpconfig` is the provider configured with the JVM Options, it is also the default provider if this parameter is not included +in the request. The target parameter is the name of the target you want to be redirected to after a successful logging in. First you are +redirected to the callback endpoint of the OpenID Connect flow (`/oidc/callback/*`) which on its turn redirects you to the location +chosen in the target parameter: + + - `JSF` is the default target, and it redirects you to the JSF frontend + - `API` redirects you to the session endpoint of the native API :ref:`oidc-session`, from which you can recover the session ID and the bearer token for the API access + - `SPA` redirects you to the new SPA, if it is already installed on your system diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index eda9778f2dd..8bcd9bef408 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -27,7 +27,8 @@ redirectURI = "#{openIdConfigBean.redirectURI}", logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}"), scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, - tokenAutoRefresh = true + tokenAutoRefresh = true, + useSession = true // If enabled state & nonce value stored in session otherwise in cookies. ) @DeclareRoles("all") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index 2baa94215da..f85279d424b 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -56,6 +56,4 @@ dataverse.oai.server.maxsets=100 #dataverse.oai.server.repositoryname= # AUTHENTICATION -dataverse.auth.oidc.bearer.max-cache-size=10000 -dataverse.auth.oidc.bearer.max-cache-age=300 payara.security.openid.sessionScopedConfiguration=true From e6033658b6bd7c7ded02a23c658d6bbb79240359 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:06:49 +0200 Subject: [PATCH 23/61] doc fix --- doc/sphinx-guides/source/api/auth.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/api/auth.rst b/doc/sphinx-guides/source/api/auth.rst index 163564580f4..6731d2c554e 100644 --- a/doc/sphinx-guides/source/api/auth.rst +++ b/doc/sphinx-guides/source/api/auth.rst @@ -76,6 +76,7 @@ Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to AP To test if bearer tokens are working, you can use a Python script that prompts you to log in to the Keycloak in a new browser window using selenium. For example, you can run the script inside the `bearer-token-example` that illustrates this: .. code-block:: bash + cd bearer-token-example ./run.sh From 0123ab2c9595793df7399807d636e3f579615155 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:25:06 +0200 Subject: [PATCH 24/61] added a release note --- doc/release-notes/PR-10905-OIDC-new-implementation.md | 8 ++++++++ doc/sphinx-guides/source/api/auth.rst | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 doc/release-notes/PR-10905-OIDC-new-implementation.md diff --git a/doc/release-notes/PR-10905-OIDC-new-implementation.md b/doc/release-notes/PR-10905-OIDC-new-implementation.md new file mode 100644 index 00000000000..4c0951622cd --- /dev/null +++ b/doc/release-notes/PR-10905-OIDC-new-implementation.md @@ -0,0 +1,8 @@ +New OpenID Connect implementation including new log in scenarios :ref:`oidc-log-in` for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Pythons cript that can be found in `bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: + +```shell + cd bearer-token-example + ./run.sh +``` + +This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. diff --git a/doc/sphinx-guides/source/api/auth.rst b/doc/sphinx-guides/source/api/auth.rst index 6731d2c554e..4806e306589 100644 --- a/doc/sphinx-guides/source/api/auth.rst +++ b/doc/sphinx-guides/source/api/auth.rst @@ -80,7 +80,7 @@ To test if bearer tokens are working, you can use a Python script that prompts y cd bearer-token-example ./run.sh -This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own script that lets users performed some custom scripted tasks. +This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. Signed URLs ----------- From b0190e56c3a4b2e9cc08572ef1c6f61d185bd055 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:27:30 +0200 Subject: [PATCH 25/61] doc fix --- doc/sphinx-guides/source/api/native-api.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index ae003d04464..8ae3e204d30 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -6461,6 +6461,7 @@ after logging-in in your browser. The returned JSON looks like this: } } + You can then use the retrieved `session` and `accessToken` for subsequent calls to the API or the session endpoint, as illustrated in the following curl examples: .. code-block:: bash From 058c17af314ac130f7926fd1346dc5c4204fbd76 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:30:35 +0200 Subject: [PATCH 26/61] doc fix --- doc/sphinx-guides/source/api/native-api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/api/native-api.rst b/doc/sphinx-guides/source/api/native-api.rst index 8ae3e204d30..bf63b562efa 100644 --- a/doc/sphinx-guides/source/api/native-api.rst +++ b/doc/sphinx-guides/source/api/native-api.rst @@ -6431,6 +6431,7 @@ Parameters: ``per_page`` Number of results returned per page. .. _oidc-session: + Session ------- @@ -6461,7 +6462,6 @@ after logging-in in your browser. The returned JSON looks like this: } } - You can then use the retrieved `session` and `accessToken` for subsequent calls to the API or the session endpoint, as illustrated in the following curl examples: .. code-block:: bash From 4e6e8e5b03ac4e860da1aa44e78a077875e789f3 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:36:12 +0200 Subject: [PATCH 27/61] doc fix --- doc/sphinx-guides/source/installation/oidc.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index bda063ec361..7e9ee9d7b29 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -147,6 +147,7 @@ The following options are available: * - ``dataverse.auth.oidc.subtitle`` - A subtitle, currently not displayed by the UI. - N + - ``OpenID Connect`` .. _oidc-log-in: Choosing provisioned providers at log in From 6a635bf67fa03770b77018e66dfb9b7d564a9552 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:38:47 +0200 Subject: [PATCH 28/61] doc fix --- doc/sphinx-guides/source/installation/oidc.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index 7e9ee9d7b29..3b7823ef66b 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -150,6 +150,7 @@ The following options are available: - ``OpenID Connect`` .. _oidc-log-in: + Choosing provisioned providers at log in ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 3cf9d4d52059f8bd70a6d51f17079b0a94bd82bf Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski Date: Mon, 7 Oct 2024 13:43:39 +0200 Subject: [PATCH 29/61] doc fix --- doc/sphinx-guides/source/installation/oidc.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index 3b7823ef66b..999d7166715 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -152,12 +152,12 @@ The following options are available: .. _oidc-log-in: Choosing provisioned providers at log in -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In the JSF frontend, you can select the provider you wish to log in with at the logging in time. However, you can also use the log in link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the browser, it will prompt you with the Keycloak and redirect you to the API endpoint for retrieving the session :ref:`oidc-session`): -`http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig`_. +http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig The `oidc` parameter is the provisioned provider ID you wish to use and is configured in the previous steps. For example, `oidc-mpconfig` is the provider configured with the JVM Options, it is also the default provider if this parameter is not included From 22da240a61a1cf232c109ca6f12224c1689b10c4 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:38:24 +0200 Subject: [PATCH 30/61] Update src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java Co-authored-by: Oliver Bertuch --- .../providers/oauth2/OAuth2LoginBackingBean.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 38404af52eb..8d23c1aaef3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -92,10 +92,9 @@ public class OAuth2LoginBackingBean implements Serializable { */ public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); - if (idp instanceof OIDCAuthProvider) { - return oidcLoginBackingBean.getLogInLink((OIDCAuthProvider) idp); + if (idp instanceof OIDCAuthProvider oidcIdP) { + return oidcLoginBackingBean.getLogInLink(oidcIdP); } - String state = createState(idp, toOption(redirectPage)); return idp.buildAuthzUrl(state, systemConfig.getOAuth2CallbackUrl()); } From 96bb495f30a1dc7eef24f7be4022fbda09a3f7df Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:43:31 +0200 Subject: [PATCH 31/61] Update src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java Co-authored-by: Oliver Bertuch --- .../authorization/providers/oauth2/OAuth2LoginBackingBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 8d23c1aaef3..7d943db9034 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -105,7 +105,7 @@ public String linkFor(String idpId, String redirectPage) { public void exchangeCodeForToken() throws IOException { HttpServletRequest req = Faces.getRequest(); final String stateParameter = req.getParameter("state"); - if (stateParameter == null || "".equals(stateParameter)) { + if (stateParameter == null || stateParameter.isEmpty()) oidcLoginBackingBean.setUser(); return; } From 5950c94124936108e3d46b60edd4b2d1a58bb237 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:49:12 +0200 Subject: [PATCH 32/61] Update doc/sphinx-guides/source/installation/oidc.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/installation/oidc.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index 999d7166715..ff4c8dcb7dc 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -154,8 +154,7 @@ The following options are available: Choosing provisioned providers at log in ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In the JSF frontend, you can select the provider you wish to log in with at the logging in time. However, you can also use the log -in link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the +In the JSF frontend, you can select the provider you wish to log in with at login time. However, you can also use the login link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the browser, it will prompt you with the Keycloak and redirect you to the API endpoint for retrieving the session :ref:`oidc-session`): http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig From 6ff8744b2878ab0b6b15d375c5be07c7cefcf884 Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:51:23 +0200 Subject: [PATCH 33/61] Update doc/sphinx-guides/source/installation/oidc.rst Co-authored-by: Philip Durbin --- doc/sphinx-guides/source/installation/oidc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index ff4c8dcb7dc..e24305d2776 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -151,7 +151,7 @@ The following options are available: .. _oidc-log-in: -Choosing provisioned providers at log in +Choosing Provisioned Providers at Log In ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In the JSF frontend, you can select the provider you wish to log in with at login time. However, you can also use the login link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the From 803618d507b19de1ca55a44ecaead66ca65b6c8a Mon Sep 17 00:00:00 2001 From: Eryk Kulikowski <101262459+ErykKul@users.noreply.github.com> Date: Mon, 7 Oct 2024 19:53:58 +0200 Subject: [PATCH 34/61] Update doc/release-notes/PR-10905-OIDC-new-implementation.md Co-authored-by: Philip Durbin --- doc/release-notes/PR-10905-OIDC-new-implementation.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/release-notes/PR-10905-OIDC-new-implementation.md b/doc/release-notes/PR-10905-OIDC-new-implementation.md index 4c0951622cd..daccb31fed8 100644 --- a/doc/release-notes/PR-10905-OIDC-new-implementation.md +++ b/doc/release-notes/PR-10905-OIDC-new-implementation.md @@ -1,4 +1,4 @@ -New OpenID Connect implementation including new log in scenarios :ref:`oidc-log-in` for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Pythons cript that can be found in `bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: +New OpenID Connect implementation including new log in scenarios (see [the guides](https://dataverse-guide--10905.org.readthedocs.build/en/10905/installation/oidc.html#choosing-provisioned-providers-at-log-in)) for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Python script that can be found in the `bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: ```shell cd bearer-token-example From 68da25aecdff56c279e359c3ee09bf68a4998702 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 20:23:02 +0200 Subject: [PATCH 35/61] removed run_dev_env.sh and added it in .gitignore to prevent commiting it --- .gitignore | 3 +++ run_dev_env.sh | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) delete mode 100755 run_dev_env.sh diff --git a/.gitignore b/.gitignore index 514f82116de..81e72a27ad0 100644 --- a/.gitignore +++ b/.gitignore @@ -63,3 +63,6 @@ src/main/webapp/resources/images/dataverseproject.png.thumb140 # Docker development volumes /docker-dev-volumes /.vs + +# custom run script for developers +run_dev_env.sh \ No newline at end of file diff --git a/run_dev_env.sh b/run_dev_env.sh deleted file mode 100755 index 67cfaa20397..00000000000 --- a/run_dev_env.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -mvn -Pct clean package docker:run \ No newline at end of file From d600c51c33aeb9313cbfb412502ecd62f0daa06b Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 20:43:25 +0200 Subject: [PATCH 36/61] moved python example --- doc/release-notes/PR-10905-OIDC-new-implementation.md | 4 ++-- .../_static/api/bearer-token-example}/get_session.py | 0 .../_static/api/bearer-token-example}/requirements.txt | 0 .../sphinx-guides/_static/api/bearer-token-example}/run.sh | 0 doc/sphinx-guides/source/api/auth.rst | 4 ++-- doc/sphinx-guides/source/installation/oidc.rst | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename {bearer-token-example => doc/sphinx-guides/_static/api/bearer-token-example}/get_session.py (100%) rename {bearer-token-example => doc/sphinx-guides/_static/api/bearer-token-example}/requirements.txt (100%) rename {bearer-token-example => doc/sphinx-guides/_static/api/bearer-token-example}/run.sh (100%) diff --git a/doc/release-notes/PR-10905-OIDC-new-implementation.md b/doc/release-notes/PR-10905-OIDC-new-implementation.md index daccb31fed8..4f2bbcf22f1 100644 --- a/doc/release-notes/PR-10905-OIDC-new-implementation.md +++ b/doc/release-notes/PR-10905-OIDC-new-implementation.md @@ -1,7 +1,7 @@ -New OpenID Connect implementation including new log in scenarios (see [the guides](https://dataverse-guide--10905.org.readthedocs.build/en/10905/installation/oidc.html#choosing-provisioned-providers-at-log-in)) for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Python script that can be found in the `bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: +New OpenID Connect implementation including new log in scenarios (see [the guides](https://dataverse-guide--10905.org.readthedocs.build/en/10905/installation/oidc.html#choosing-provisioned-providers-at-log-in)) for the current JSF frontend, the new Single Page Application (SPA) frontend, and a generic API usage. The API scenario using Bearer Token authorization is illustrated with a Python script that can be found in the `doc/sphinx-guides/_static/api/bearer-token-example` directory. This Python script prompts you to log in to the Keycloak in a new browser window using selenium. You can run that script with the following commands: ```shell - cd bearer-token-example + cd doc/sphinx-guides/_static/api/bearer-token-example ./run.sh ``` diff --git a/bearer-token-example/get_session.py b/doc/sphinx-guides/_static/api/bearer-token-example/get_session.py similarity index 100% rename from bearer-token-example/get_session.py rename to doc/sphinx-guides/_static/api/bearer-token-example/get_session.py diff --git a/bearer-token-example/requirements.txt b/doc/sphinx-guides/_static/api/bearer-token-example/requirements.txt similarity index 100% rename from bearer-token-example/requirements.txt rename to doc/sphinx-guides/_static/api/bearer-token-example/requirements.txt diff --git a/bearer-token-example/run.sh b/doc/sphinx-guides/_static/api/bearer-token-example/run.sh similarity index 100% rename from bearer-token-example/run.sh rename to doc/sphinx-guides/_static/api/bearer-token-example/run.sh diff --git a/doc/sphinx-guides/source/api/auth.rst b/doc/sphinx-guides/source/api/auth.rst index 4806e306589..4cb4a66a455 100644 --- a/doc/sphinx-guides/source/api/auth.rst +++ b/doc/sphinx-guides/source/api/auth.rst @@ -73,11 +73,11 @@ Bearer tokens are defined in `RFC 6750`_ and can be used as an alternative to AP .. _RFC 6750: https://tools.ietf.org/html/rfc6750 -To test if bearer tokens are working, you can use a Python script that prompts you to log in to the Keycloak in a new browser window using selenium. For example, you can run the script inside the `bearer-token-example` that illustrates this: +To test if bearer tokens are working, you can use a Python script that prompts you to log in to the Keycloak in a new browser window using selenium. For example, you can run the script inside the `doc/sphinx-guides/_static/api/bearer-token-example` that illustrates this: .. code-block:: bash - cd bearer-token-example + cd doc/sphinx-guides/_static/api/bearer-token-example ./run.sh This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. diff --git a/doc/sphinx-guides/source/installation/oidc.rst b/doc/sphinx-guides/source/installation/oidc.rst index e24305d2776..6fabdbe339c 100644 --- a/doc/sphinx-guides/source/installation/oidc.rst +++ b/doc/sphinx-guides/source/installation/oidc.rst @@ -154,7 +154,7 @@ The following options are available: Choosing Provisioned Providers at Log In ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -In the JSF frontend, you can select the provider you wish to log in with at login time. However, you can also use the login link directly, for example, from a Python script as illustrated in the `bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the +In the JSF frontend, you can select the provider you wish to log in with at login time. However, you can also use the login link directly, for example, from a Python script as illustrated in the `doc/sphinx-guides/_static/api/bearer-token-example` :ref:`bearer-tokens` (you can copy that link in the browser, it will prompt you with the Keycloak and redirect you to the API endpoint for retrieving the session :ref:`oidc-session`): http://localhost:8080/oidc/login?target=API&oidcp=oidc-mpconfig From 817c416bfb36d9657a6d218c140b6f2e01739652 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 20:55:00 +0200 Subject: [PATCH 37/61] restored accidently deleted line --- .../authorization/providers/oauth2/OAuth2LoginBackingBean.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 7d943db9034..c001d3dcd1d 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -95,6 +95,7 @@ public String linkFor(String idpId, String redirectPage) { if (idp instanceof OIDCAuthProvider oidcIdP) { return oidcLoginBackingBean.getLogInLink(oidcIdP); } + String state = createState(idp, toOption(redirectPage)); return idp.buildAuthzUrl(state, systemConfig.getOAuth2CallbackUrl()); } @@ -105,7 +106,7 @@ public String linkFor(String idpId, String redirectPage) { public void exchangeCodeForToken() throws IOException { HttpServletRequest req = Faces.getRequest(); final String stateParameter = req.getParameter("state"); - if (stateParameter == null || stateParameter.isEmpty()) + if (stateParameter == null || stateParameter.isEmpty()) { oidcLoginBackingBean.setUser(); return; } From 45facde3a8bb7d77cb2673f770073ecfe9b3888d Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:01:34 +0200 Subject: [PATCH 38/61] @ejb -> @inject --- src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index f0479ec7103..88a3949a079 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -249,7 +249,7 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletResponse httpResponse; - @EJB + @Inject OIDCLoginBackingBean oidcLoginBackingBean; @Inject From 9a443805b752d622770d6273153194724a9100ac Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:06:08 +0200 Subject: [PATCH 39/61] toJson -> JsonPrinter.json --- src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java index 1c0fa67b7ec..3e5895b5fa1 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/OIDCSession.java @@ -5,6 +5,7 @@ import edu.harvard.iq.dataverse.authorization.AuthenticationServiceBean; import edu.harvard.iq.dataverse.authorization.UserRecordIdentifier; import edu.harvard.iq.dataverse.authorization.users.AuthenticatedUser; +import edu.harvard.iq.dataverse.util.json.JsonPrinter; import fish.payara.security.openid.api.OpenIdContext; import jakarta.ejb.Stateless; import jakarta.inject.Inject; @@ -41,7 +42,7 @@ public Response session(@Context ContainerRequestContext crc) { try { return ok( jsonObjectBuilder() - .add("user", authUser.toJson()) + .add("user", JsonPrinter.json(authUser)) .add("session", crc.getCookies().get("JSESSIONID").getValue()) .add("accessToken", openIdContext.getAccessToken().getToken())); } catch (Exception e) { From ef0f0f82d8de9280d197e4132931059d744f2e81 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:09:09 +0200 Subject: [PATCH 40/61] change BearerTokenMechanism class to final class --- .../providers/oauth2/oidc/BearerTokenMechanism.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java index 9fbbaaa24f5..dd3be2b597c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java @@ -20,7 +20,7 @@ @ApplicationScoped @DeclareRoles({ "all" }) -public class BearerTokenMechanism extends BearerGroupsIdentityStore { +public final class BearerTokenMechanism extends BearerGroupsIdentityStore { private static final Logger logger = Logger.getLogger(OIDCLoginBackingBean.class.getName()); @Inject From 0658a93efec21b3fd538a2a2f851adfa83440938 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:12:45 +0200 Subject: [PATCH 41/61] added comment --- .../authorization/providers/oauth2/OAuth2LoginBackingBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index c001d3dcd1d..2f7026dea5a 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -106,6 +106,7 @@ public String linkFor(String idpId, String redirectPage) { public void exchangeCodeForToken() throws IOException { HttpServletRequest req = Faces.getRequest(); final String stateParameter = req.getParameter("state"); + // if no state is present, we know this is OIDC and can return early if (stateParameter == null || stateParameter.isEmpty()) { oidcLoginBackingBean.setUser(); return; From 6148051ebd92e09ac0177da4b28addd52e613acf Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:15:28 +0200 Subject: [PATCH 42/61] added comment --- .../authorization/providers/oauth2/OAuth2LoginBackingBean.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java index 2f7026dea5a..9d57d7d8170 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/OAuth2LoginBackingBean.java @@ -93,6 +93,7 @@ public class OAuth2LoginBackingBean implements Serializable { public String linkFor(String idpId, String redirectPage) { AbstractOAuth2AuthenticationProvider idp = authenticationSvc.getOAuth2Provider(idpId); if (idp instanceof OIDCAuthProvider oidcIdP) { + // OIDC has its own Log In endpoint, we use that one instead return oidcLoginBackingBean.getLogInLink(oidcIdP); } String state = createState(idp, toOption(redirectPage)); From ca5ee8257265f47217408ab802d8737b668252b4 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:20:25 +0200 Subject: [PATCH 43/61] removed unused injection --- .../providers/oauth2/oidc/OpenIDAuthentication.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 8bcd9bef408..3473640362f 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -33,9 +33,6 @@ @DeclareRoles("all") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) public class OpenIDAuthentication extends HttpServlet { - @EJB - OpenIDConfigBean openIdConfigBean; - @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); From b56210b27766bea85ed413a5caaa8a8f981c6d0b Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:34:36 +0200 Subject: [PATCH 44/61] This is Javagit add -A! (Spartan kick here) --- .../providers/oauth2/oidc/OpenIDConfigBean.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java index c1352e5ee28..ca39afafea4 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java @@ -17,7 +17,7 @@ public class OpenIDConfigBean implements java.io.Serializable { public String getProviderURI() { final String oidcp = request.getParameter("oidcp"); - if (oidcp == null || oidcp == "") { + if (oidcp == null || oidcp.isBlank()) { return JvmSettings.OIDC_AUTH_SERVER_URL.lookupOptional().orElse(null); } try { @@ -29,7 +29,7 @@ public String getProviderURI() { public String getClientId() { final String oidcp = request.getParameter("oidcp"); - if (oidcp == null || oidcp == "") { + if (oidcp == null || oidcp.isBlank()) { return JvmSettings.OIDC_CLIENT_ID.lookupOptional().orElse(null); } try { @@ -41,7 +41,7 @@ public String getClientId() { public String getClientSecret() { final String oidcp = request.getParameter("oidcp"); - if (oidcp == null || oidcp == "") { + if (oidcp == null || oidcp.isBlank()) { return JvmSettings.OIDC_CLIENT_SECRET.lookupOptional().orElse(null); } try { @@ -53,7 +53,7 @@ public String getClientSecret() { public String getRedirectURI() { String target = request.getParameter("target"); - target = target == null || target == "" ? "API" : target; + target = target == null || target.isBlank() ? "API" : target; return SystemConfig.getDataverseSiteUrlStatic() + "/oidc/callback/" + target; } From 139c9fc94f7b4cfc0073daeb49e32e8d1f6dc144 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 21:53:29 +0200 Subject: [PATCH 45/61] reverted token auto-refreshing to default value --- .../providers/oauth2/oidc/OpenIDAuthentication.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 3473640362f..6c0b3c23f21 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -27,7 +27,6 @@ redirectURI = "#{openIdConfigBean.redirectURI}", logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}"), scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, - tokenAutoRefresh = true, useSession = true // If enabled state & nonce value stored in session otherwise in cookies. ) @DeclareRoles("all") From 28d70aaa334bd3a3d388e700aae77cf0e9c1d350 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 22:13:20 +0200 Subject: [PATCH 46/61] made the fact that nobody has access to the content behind the authentication servlet by naming the role nobodyHasAccess i.s.o. all --- .../providers/oauth2/oidc/OpenIDAuthentication.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 6c0b3c23f21..299ba9f9f4c 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -29,7 +29,7 @@ scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, useSession = true // If enabled state & nonce value stored in session otherwise in cookies. ) -@DeclareRoles("all") +@DeclareRoles("nobodyHasAccess") @ServletSecurity(@HttpConstraint(rolesAllowed = "all")) public class OpenIDAuthentication extends HttpServlet { @Override From 1ffdf70673540e4b28e8d3f8463c518863c064bd Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 22:34:29 +0200 Subject: [PATCH 47/61] renamed required role: all -> nobodyHasAccess --- .../providers/oauth2/oidc/OpenIDAuthentication.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 299ba9f9f4c..15c51faa625 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -7,7 +7,6 @@ import fish.payara.security.annotations.OpenIdAuthenticationDefinition; import fish.payara.security.openid.api.OpenIdConstant; import jakarta.annotation.security.DeclareRoles; -import jakarta.ejb.EJB; import jakarta.servlet.ServletException; import jakarta.servlet.annotation.HttpConstraint; import jakarta.servlet.annotation.ServletSecurity; @@ -30,7 +29,7 @@ useSession = true // If enabled state & nonce value stored in session otherwise in cookies. ) @DeclareRoles("nobodyHasAccess") -@ServletSecurity(@HttpConstraint(rolesAllowed = "all")) +@ServletSecurity(@HttpConstraint(rolesAllowed = "nobodyHasAccess")) public class OpenIDAuthentication extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { From 5057c617cef8654a51c5c566f26ff721d256e1ca Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 22:39:41 +0200 Subject: [PATCH 48/61] SEVERE -> FINE --- .../providers/oauth2/oidc/OIDCLoginBackingBean.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index 116ae11c37e..d7cd2861103 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -135,7 +135,7 @@ private String getVerifiedEmail() { emailVerified = false; } if (!emailVerified) { - logger.log(Level.SEVERE, + logger.log(Level.FINE, "email not verified: " + openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL)); return null; } From fc5fb0fe9bedf139a586990bd457e3a5c69d2fc3 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 22:41:40 +0200 Subject: [PATCH 49/61] simplified method a bit --- .../providers/oauth2/oidc/OIDCLoginBackingBean.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index d7cd2861103..24ebb0200cb 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -121,13 +121,9 @@ public UserRecordIdentifier getUserRecordIdentifier() { private String getVerifiedEmail() { try { - if (openIdContext.getAccessToken().isExpired()) { - return null; - } final Object emailVerifiedObject = openIdContext.getClaimsJson().get(OpenIdConstant.EMAIL_VERIFIED); final boolean emailVerified; - if (emailVerifiedObject instanceof JsonValue) { - final JsonValue v = (JsonValue) emailVerifiedObject; + if (emailVerifiedObject instanceof JsonValue v) { emailVerified = JsonValue.TRUE.equals(v) || (JsonValue.ValueType.STRING.equals(v.getValueType()) && Boolean.getBoolean(((JsonString) v).getString())); From a30647f27127e16722a8c07078a87a543b4de5bc Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Mon, 7 Oct 2024 23:01:15 +0200 Subject: [PATCH 50/61] removed nimbus dependency --- pom.xml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pom.xml b/pom.xml index edf72067976..4d6e1f70823 100644 --- a/pom.xml +++ b/pom.xml @@ -446,12 +446,6 @@ scribejava-apis 6.9.0 - - - com.nimbusds - oauth2-oidc-sdk - 10.13.2 - com.github.ben-manes.caffeine From 9a4a702ace30a3cf37665bde7e716c8e9c6f5ba2 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 10:49:58 +0200 Subject: [PATCH 51/61] added comments in the code --- .../iq/dataverse/api/AbstractApiBean.java | 12 ++++++++++++ .../oauth2/oidc/BearerTokenMechanism.java | 10 ++++++++++ .../oauth2/oidc/OIDCLoginBackingBean.java | 15 +++++++++++++-- .../oauth2/oidc/OpenIDAuthentication.java | 18 ++++++++++++++++++ .../providers/oauth2/oidc/OpenIDCallback.java | 13 +++++++++++++ .../oauth2/oidc/OpenIDConfigBean.java | 9 +++++++++ .../META-INF/microprofile-config.properties | 3 +++ .../META-INF/microprofile-config.properties | 5 +++++ 8 files changed, 83 insertions(+), 2 deletions(-) diff --git a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java index 88a3949a079..e1a6acbf42e 100644 --- a/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/api/AbstractApiBean.java @@ -249,6 +249,14 @@ String getWrappedMessageWhenJson() { @Context protected HttpServletResponse httpResponse; +/** +* OIDCLoginBackingBean and SecurityContext injections are a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ @Inject OIDCLoginBackingBean oidcLoginBackingBean; @@ -337,8 +345,12 @@ protected AuthenticatedUser getRequestAuthenticatedUserOrDie(ContainerRequestCon if (requestUser.isAuthenticated()) { return (AuthenticatedUser) requestUser; } else { + // This is a part of the OpenID Connect solution using security annotations. + // try authenticating with OpenIdContext first final UserRecordIdentifier userRecordIdentifier = oidcLoginBackingBean.getUserRecordIdentifier(); if (userRecordIdentifier == null) { + // Try SecurityContext and the underlying Bearer token IdentityStore + // See: edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism AuthenticationStatus status = securityContext.authenticate(httpRequest, httpResponse, null); if (AuthenticationStatus.SUCCESS.equals(status)) { try { diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java index dd3be2b597c..1f9605ff26b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/BearerTokenMechanism.java @@ -18,6 +18,15 @@ import jakarta.inject.Inject; import jakarta.servlet.http.HttpServletRequest; +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + @ApplicationScoped @DeclareRoles({ "all" }) public final class BearerTokenMechanism extends BearerGroupsIdentityStore { @@ -42,6 +51,7 @@ protected Set getCallerGroups(AccessTokenCallerPrincipal callerPrincipal .collect(Collectors.toUnmodifiableList()).get(0); final UserRecordIdentifier userRecordIdentifier = new UserRecordIdentifier(provider.getId(), subject); final AuthenticatedUser dvUser = authenticationSvc.lookupUser(userRecordIdentifier); + // pass the authenticated user to the AbstractAPIBean that uses SecurityContext to trigger this code request.setAttribute(ApiConstants.CONTAINER_REQUEST_CONTEXT_USER, dvUser); logger.log(Level.FINE, "user found: " + dvUser.toJson()); return Set.of("all"); diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java index 24ebb0200cb..d98b8393d06 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OIDCLoginBackingBean.java @@ -29,8 +29,17 @@ import jakarta.json.JsonValue; /** - * Backing bean of the OIDC login process. Used from the login and the - * callback pages. +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * Backing bean of the OIDC login process. Used from the login and the callback pages. + * It also provides UserRecordIdentifier retrieval method used in the AbstractAPIBean for OpenIdContext processing to identify the connected user. */ @Stateless @Named @@ -150,6 +159,8 @@ private OIDCAuthProvider getProvider() { "Issuer URL (iss) not found in " + openIdContext.getAccessToken().getJwtClaims().toString()); return null; } + // Are we sure these values are equal? Does the issuer URL have to be the full qualified one or could it be just the "top" URL from where you can access the .well-known endpoint? + // - No, not sure. This might cause problems in the future. List providers = authenticationSvc.getAuthenticationProviderIdsOfType(OIDCAuthProvider.class) .stream() .map(providerId -> (OIDCAuthProvider) authenticationSvc.getAuthenticationProvider(providerId)) diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java index 15c51faa625..9809d4152f3 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDAuthentication.java @@ -15,6 +15,22 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * If we want to use the OIDC annotation, it assumes we also use the security annotations, HttpAuthenticationMechanism, IdentityStoreHandler and the IdentityStore (which we do not use...) and that the endpoints security relies on SecurityContext. + * If I do not add the security annotation (@ServletSecurity(@HttpConstraint(rolesAllowed = "nobodyHasAccess"))) to this authentication servlet, it will not be secured and will not force you to log in, you will just see its content, OIDC annotation has no effect in that situation. + * The authentication servlet is the only location that is really secured with OIDC and security annotations, SecurityContext, IdentityStoreHandler, HttpAuthenticationMechanism and the bearer token IdentityStore. But we do not use it anywhere else (and I assume we do not want to), so this servlet has content that is unreachable (because we do not use security annotations anywhere else in authentication mechanisms we implemented, so you cannot gain access to it by any means). + * You can only make calls to the API with the bearer tokens in this implementation because the bearer token IdentityStore passes the user to the AbstractApiBean, so it can just rely on the "standard" Dataverse implementation, without any security annotations. The authentication servlet is there only to force the use of the OIDC annotation to do its work, so it is not important that it is not reachable. After logging in you are redirected somewhere else anyway. + */ + /** * OIDC login implementation */ @@ -27,12 +43,14 @@ logout = @LogoutDefinition(redirectURI = "#{openIdConfigBean.logoutURI}"), scope = {OpenIdConstant.OPENID_SCOPE, OpenIdConstant.EMAIL_SCOPE, OpenIdConstant.PROFILE_SCOPE}, useSession = true // If enabled state & nonce value stored in session otherwise in cookies. + // I do not know if useSession should default to true. I made it explicit because of the comments about the Payara jsession being insecure and that we do not want to use cookies. It looks like these requirements would make this implementation unusable: either jsession cookie or token cookie is used. We might end up not using this code at all. ) @DeclareRoles("nobodyHasAccess") @ServletSecurity(@HttpConstraint(rolesAllowed = "nobodyHasAccess")) public class OpenIDAuthentication extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { + // as explained above, this content is unreachable final String baseURL = SystemConfig.getDataverseSiteUrlStatic(); final String target = request.getParameter("target"); final String redirect = "SPA".equals(target) ? baseURL + "/spa/" : baseURL; diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java index cb956c36e87..871cbe59650 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDCallback.java @@ -9,6 +9,19 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + +/** + * I am not sure if we need the redirects implemented here, this entire code might be removed in the future. I think it depends if we want to allow cookies or not. If we assume that nobody wants to use httponly secure cookies as being OK for security and always implement local storage/in memory tokens, like many systems do, then we can delete this routing thing. I just like the simplicity of it. You cannot turn the cookies off anyway if you want to use the OIDC annotation on payara, so we can have some convenience code as well. I see it more like that: either we happily use what is possible on Payara/Jakarta, or we implement everything ourselves, like it was the case until now. If we do use the OIDC annotation, we might as well use the convenience of it and not hide it under a rug for some hacker to figure it out anyway. + */ + @WebServlet("/oidc/callback/*") public class OpenIDCallback extends HttpServlet { @Override diff --git a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java index ca39afafea4..b2d5183447b 100644 --- a/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java +++ b/src/main/java/edu/harvard/iq/dataverse/authorization/providers/oauth2/oidc/OpenIDConfigBean.java @@ -7,6 +7,15 @@ import jakarta.inject.Named; import jakarta.servlet.http.HttpServletRequest; +/** +* This code is a part of an OpenID Connect solutions using Jakarta security annotations. +* The main building blocks are: +* - @OpenIdAuthenticationDefinition added on the authentication HttpServlet edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.OpenIDAuthentication, see https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html +* - IdentityStoreHandler and HttpAuthenticationMechanism, as provided on the server (no custom implementation involved here), see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-auth +* - IdentityStore implemented for Bearer tokens in edu.harvard.iq.dataverse.authorization.providers.oauth2.oidc.BearerTokenMechanism, see also https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html and https://hantsy.gitbook.io/java-ee-8-by-example/security/security-store +* - SecurityContext injected in AbstractAPIBean to handle authentication, see https://hantsy.gitbook.io/java-ee-8-by-example/security/security-context +*/ + @Named("openIdConfigBean") public class OpenIDConfigBean implements java.io.Serializable { @Inject diff --git a/src/main/resources/META-INF/microprofile-config.properties b/src/main/resources/META-INF/microprofile-config.properties index f85279d424b..69e72cccf6e 100644 --- a/src/main/resources/META-INF/microprofile-config.properties +++ b/src/main/resources/META-INF/microprofile-config.properties @@ -56,4 +56,7 @@ dataverse.oai.server.maxsets=100 #dataverse.oai.server.repositoryname= # AUTHENTICATION +# the following setting enables the Multi-tenancy Support +# for the OIDC securitya nnotation base implementation +# see: https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html#multitenancy payara.security.openid.sessionScopedConfiguration=true diff --git a/src/test/resources/META-INF/microprofile-config.properties b/src/test/resources/META-INF/microprofile-config.properties index 378e65a120a..8233eeddce1 100644 --- a/src/test/resources/META-INF/microprofile-config.properties +++ b/src/test/resources/META-INF/microprofile-config.properties @@ -16,4 +16,9 @@ test.filesDir=/tmp/dataverse dataverse.files.directory=${test.filesDir} dataverse.files.uploads=${test.filesDir}/uploads dataverse.files.docroot=${test.filesDir}/docroot + +# AUTHENTICATION +# the following setting enables the Multi-tenancy Support +# for the OIDC securitya nnotation base implementation +# see: https://docs.payara.fish/enterprise/docs/Technical%20Documentation/Public%20API/OpenID%20Connect%20Support.html#multitenancy payara.security.openid.sessionScopedConfiguration=true From e1b75f9bca055b09eff66cdd477388e336dfa508 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 12:08:54 +0200 Subject: [PATCH 52/61] updated release note --- doc/release-notes/PR-10905-OIDC-new-implementation.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/doc/release-notes/PR-10905-OIDC-new-implementation.md b/doc/release-notes/PR-10905-OIDC-new-implementation.md index 4f2bbcf22f1..09147b8302b 100644 --- a/doc/release-notes/PR-10905-OIDC-new-implementation.md +++ b/doc/release-notes/PR-10905-OIDC-new-implementation.md @@ -6,3 +6,13 @@ New OpenID Connect implementation including new log in scenarios (see [the guide ``` This script is safe for production use, as it does not require you to know the client secret or the user credentials. Therefore, you can safely distribute it as a part of your own Python script that lets users run some custom tasks. + +The following settings become deprecated with this change and can be removed from the configuration: +- `dataverse.auth.oidc.pkce.enabled` +- `dataverse.auth.oidc.pkce.method` +- `dataverse.auth.oidc.pkce.max-cache-size` +- `dataverse.auth.oidc.pkce.max-cache-age` + +Also, the bearer token authentication is now always enabled. Therefore, the `dataverse.feature.api-bearer-auth` feature flag is no longer used and can be removed from the configuration as well. + +The new implementation relies now on the builtin OIDC support in our application server (Payara). With this change the Nimbus SDK is no longer used and is removed from the dependencies. From b90bb311c39f4e67c6f82ec3530c13a4ab993f2f Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 15:48:05 +0200 Subject: [PATCH 53/61] PKCE client example --- .../frontend/PKCE-example/PKCE-example.html | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html diff --git a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html new file mode 100644 index 00000000000..9ca5068d580 --- /dev/null +++ b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html @@ -0,0 +1,23 @@ + + + + + + + + + + + \ No newline at end of file From 56c7ade4e532bd411da67753d1efdacae379124c Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 16:39:52 +0200 Subject: [PATCH 54/61] removed unneeded newline --- .../_static/frontend/PKCE-example/PKCE-example.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html index 9ca5068d580..05c61b6f080 100644 --- a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html +++ b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html @@ -10,8 +10,7 @@ url: "http://keycloak.mydomain.com:8090", realm: "test", clientId: "test" - } - ); + }); kc.init({ pkceMethod: 'S256', redirectUri: 'http://localhost:8080/api/v1/users/:me' From c151b5c01e55de9273ddfcaa9c3f95bcf2cc8e59 Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 16:42:03 +0200 Subject: [PATCH 55/61] added ; at the end of the line --- .../_static/frontend/PKCE-example/PKCE-example.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html index 05c61b6f080..9e80ce32d3b 100644 --- a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html +++ b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html @@ -15,7 +15,7 @@ pkceMethod: 'S256', redirectUri: 'http://localhost:8080/api/v1/users/:me' }); - kc.login() + kc.login(); From f219d798b624a2d10a81c4622854bda20d10f56b Mon Sep 17 00:00:00 2001 From: Eryk Kullikowski Date: Tue, 8 Oct 2024 16:51:15 +0200 Subject: [PATCH 56/61] double quotes to single quotes --- .../_static/frontend/PKCE-example/PKCE-example.html | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html index 9e80ce32d3b..e70abbb9030 100644 --- a/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html +++ b/doc/sphinx-guides/_static/frontend/PKCE-example/PKCE-example.html @@ -7,9 +7,9 @@