From 0db212f5ce415fdab0ee22fcfacc4b7d3a59cb4b Mon Sep 17 00:00:00 2001 From: Martin Weise <martin.weise@tuwien.ac.at> Date: Wed, 29 Mar 2023 14:18:53 +0200 Subject: [PATCH] Large update - Added JWT check on container, database, table, query, identifier service - Removed all gateway check to the authentication service, instead check the token validity directly in the service - Removed some env vars --- .env.unix.example | 28 ++-- docker-compose.yml | 2 + fda-authentication-service/Dockerfile | 4 +- fda-authentication-service/dbrepo-realm.json | 124 ++++++++++++++---- fda-broker-service/rabbitmq.conf | 2 +- fda-container-service/Dockerfile | 4 +- fda-container-service/pom.xml | 7 + .../src/main/resources/application-docker.yml | 3 + .../src/main/resources/application-local.yml | 5 +- .../src/main/resources/application.yml | 3 + .../at/tuwien/auth/AuthTokenFilterTest.java | 11 -- .../AuthenticationServiceGatewayTest.java | 76 ----------- .../java/at/tuwien/auth/AuthTokenFilter.java | 56 +++++++- .../at/tuwien/config/WebSecurityConfig.java | 11 +- .../gateway/AuthenticationServiceGateway.java | 16 --- .../AuthenticationServiceGatewayImpl.java | 64 --------- fda-database-service/Dockerfile | 4 +- fda-database-service/pom.xml | 7 + .../src/main/resources/application-docker.yml | 3 + .../src/main/resources/application-local.yml | 5 +- .../src/main/resources/application.yml | 3 + .../at/tuwien/auth/AuthTokenFilterTest.java | 9 -- .../AuthenticationServiceGatewayTest.java | 87 ------------ .../java/at/tuwien/auth/AuthTokenFilter.java | 60 +++++++-- .../at/tuwien/config/WebSecurityConfig.java | 11 +- .../gateway/AuthenticationServiceGateway.java | 16 --- .../AuthenticationServiceGatewayImpl.java | 66 ---------- fda-identifier-service/Dockerfile | 4 + fda-identifier-service/pom.xml | 7 + .../src/main/resources/application-docker.yml | 5 + .../src/main/resources/application-local.yml | 5 + .../src/main/resources/application.yml | 5 + .../at/tuwien/auth/AuthTokenFilterTest.java | 11 -- .../AuthenticationServiceGatewayTest.java | 81 ------------ .../java/at/tuwien/auth/AuthTokenFilter.java | 60 +++++++-- .../at/tuwien/config/WebSecurityConfig.java | 11 +- .../gateway/AuthenticationServiceGateway.java | 17 --- .../AuthenticationServiceGatewayImpl.java | 49 ------- fda-query-service/Dockerfile | 4 + fda-query-service/pom.xml | 7 + .../src/main/resources/application-docker.yml | 5 + .../src/main/resources/application-local.yml | 5 + .../src/main/resources/application.yml | 5 + .../at/tuwien/auth/AuthTokenFilterTest.java | 9 -- .../java/at/tuwien/auth/AuthTokenFilter.java | 60 +++++++-- .../at/tuwien/config/WebSecurityConfig.java | 11 +- .../gateway/AuthenticationServiceGateway.java | 16 --- .../AuthenticationServiceGatewayImpl.java | 49 ------- fda-table-service/Dockerfile | 4 + fda-table-service/pom.xml | 7 + .../src/main/resources/application-docker.yml | 5 + .../src/main/resources/application-local.yml | 5 + .../src/main/resources/application.yml | 5 + .../java/at/tuwien/auth/AuthTokenFilter.java | 58 +++++++- .../at/tuwien/config/WebSecurityConfig.java | 11 +- .../gateway/AuthenticationServiceGateway.java | 14 -- .../AuthenticationServiceGatewayImpl.java | 38 ------ 57 files changed, 491 insertions(+), 769 deletions(-) delete mode 100644 fda-container-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java delete mode 100644 fda-container-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java delete mode 100644 fda-container-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java delete mode 100644 fda-database-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java delete mode 100644 fda-database-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java delete mode 100644 fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java delete mode 100644 fda-identifier-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java delete mode 100644 fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java delete mode 100644 fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java delete mode 100644 fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java delete mode 100644 fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java delete mode 100644 fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java delete mode 100644 fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java diff --git a/.env.unix.example b/.env.unix.example index 2919f4e34a..3f39fd7b37 100644 --- a/.env.unix.example +++ b/.env.unix.example @@ -1,24 +1,16 @@ +DBREPO_CLIENT_SECRET=MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG +RABBITMQ_CLIENT_SECRET=JEC2FexxrX4N65fLeDGukAl6R3Lc9y0u +JWT_ISSUER=http://localhost:8080/realms/dbrepo +JWT_PUBKEY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB +SHARED_FILESYSTEM=/tmp +LOG_LEVEL=trace +ELASTIC_PASSWORD=elastic METADATA_DB=fda METADATA_USERNAME=root METADATA_PASSWORD=dbrepo BROKER_USERNAME=fda BROKER_PASSWORD=fda +KEYCLOAK_ADMIN=fda +KEYCLOAK_ADMIN_PASSWORD=fda BROKER_CONSUMERS=2 -WEBSITE=http://example.com -TOKEN_MAX=5 -MAIL_FROM="Database Repository <noreply@example.com>" -MAIL_REPLY_TO="Admin <somebody@example.com>" -MAIL_VERIFY=false -SMTP_HOST= -SMTP_PORT= -SMTP_USERNAME= -SMTP_PASSWORD= -JWT_ISSUER=dbrepo -JWT_SECRET=secret -JWT_EXPIRATION=86400000 -SHARED_FILESYSTEM=/tmp -LOG_LEVEL=trace # error, warning, info, debug, trace -DEFAULT_ROLES=ROLE_RESEARCHER -SUPERUSERS=user1,user2 -ELASTIC_PASSWORD=elastic -CLIENT_SECRET=client-secret \ No newline at end of file +WEBSITE=http://example.com \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 3d1870ea7a..2ec72e2df9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -282,6 +282,8 @@ services: condition: service_healthy fda-metadata-db: condition: service_healthy + fda-authentication-service: + condition: service_healthy logging: driver: json-file diff --git a/fda-authentication-service/Dockerfile b/fda-authentication-service/Dockerfile index 3cbf6b9295..38e737360b 100644 --- a/fda-authentication-service/Dockerfile +++ b/fda-authentication-service/Dockerfile @@ -49,8 +49,8 @@ ENV KC_DB_PASSWORD=${METADATA_PASSWORD} ENV KC_HOSTNAME=localhost ENV KEYCLOAK_IMPORT=/opt/keycloak/data/import/dbrepo-realm.json -ENV KEYCLOAK_ADMIN=keycloak -ENV KEYCLOAK_ADMIN_PASSWORD=keycloak +ENV KEYCLOAK_ADMIN=fda +ENV KEYCLOAK_ADMIN_PASSWORD=fda HEALTHCHECK --interval=10s --timeout=5s --retries=12 CMD ["bash", "/app/healthcheck.sh"] diff --git a/fda-authentication-service/dbrepo-realm.json b/fda-authentication-service/dbrepo-realm.json index bfe28ab1e0..e4df97e0b8 100644 --- a/fda-authentication-service/dbrepo-realm.json +++ b/fda-authentication-service/dbrepo-realm.json @@ -696,6 +696,7 @@ "attributes" : { } } ], "security-admin-console" : [ ], + "dbrepo-client" : [ ], "admin-cli" : [ ], "rabbitmq-client" : [ ], "account-console" : [ ], @@ -842,7 +843,7 @@ "otpPolicyLookAheadWindow" : 1, "otpPolicyPeriod" : 30, "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppGoogleName", "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName" ], + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppMicrosoftAuthenticatorName", "totpAppGoogleName" ], "webAuthnPolicyRpEntityName" : "keycloak", "webAuthnPolicySignatureAlgorithms" : [ "ES256" ], "webAuthnPolicyRpId" : "", @@ -994,6 +995,73 @@ "nodeReRegistrationTimeout" : 0, "defaultClientScopes" : [ "web-origins", "acr", "profile", "roles", "email" ], "optionalClientScopes" : [ "address", "phone", "offline_access", "microprofile-jwt" ] + }, { + "id" : "6b7ef364-4132-4831-b4e2-b6e9e9dc63ee", + "clientId" : "dbrepo-client", + "name" : "${dbrepo-client}", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : true, + "clientAuthenticatorType" : "client-secret", + "secret" : "MUwRc7yfXSJwX8AdRMWaQC3Nep1VjwgG", + "redirectUris" : [ "*" ], + "webOrigins" : [ "*" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "oidc.ciba.grant.enabled" : "false", + "client.secret.creation.time" : "1680085365", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "*", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "protocolMappers" : [ { + "id" : "6a8cae99-294f-4fc2-9561-5a52f3f6a1ba", + "name" : "Audience", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-hardcoded-claim-mapper", + "consentRequired" : false, + "config" : { + "claim.value" : "spring", + "userinfo.token.claim" : "false", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "aud", + "access.tokenResponse.claim" : "false" + } + }, { + "id" : "ef081a47-f023-4056-958c-4194d3878d8c", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "userinfo.token.claim" : "false", + "user.attribute" : "username", + "id.token.claim" : "false", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ ], + "optionalClientScopes" : [ "rabbitmq.read:*/*", "web-origins", "acr", "rabbitmq.write:*/*", "address", "phone", "offline_access", "profile", "roles", "microprofile-jwt", "email", "rabbitmq.configure:*/*" ] }, { "id" : "25741f6b-4867-4138-8238-6345c6ba8702", "clientId" : "rabbitmq-client", @@ -1579,7 +1647,7 @@ }, { "id" : "c96f0b73-ea79-4b46-93ef-d1092297f855", "name" : "rabbitmq.read:*/*", - "description" : "RabbitMQ Read All", + "description" : "", "protocol" : "openid-connect", "attributes" : { "include.in.token.scope" : "true", @@ -1632,8 +1700,8 @@ } } ] } ], - "defaultDefaultClientScopes" : [ "rabbitmq.read:*/*" ], - "defaultOptionalClientScopes" : [ "rabbitmq.write:*/*", "offline_access", "rabbitmq.configure:*/*", "roles", "role_list", "address", "phone", "acr", "microprofile-jwt", "email", "profile", "web-origins" ], + "defaultDefaultClientScopes" : [ ], + "defaultOptionalClientScopes" : [ "rabbitmq.write:*/*", "offline_access", "rabbitmq.configure:*/*", "roles", "role_list", "address", "phone", "acr", "microprofile-jwt", "email", "profile", "rabbitmq.read:*/*", "web-origins" ], "browserSecurityHeaders" : { "contentSecurityPolicyReportOnly" : "", "xContentTypeOptions" : "nosniff", @@ -1710,7 +1778,7 @@ "subType" : "authenticated", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "saml-user-attribute-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-role-list-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "saml-user-property-mapper" ] } }, { "id" : "3ab11d74-5e76-408a-b85a-26bf8950f979", @@ -1719,7 +1787,7 @@ "subType" : "anonymous", "subComponents" : { }, "config" : { - "allowed-protocol-mapper-types" : [ "oidc-full-name-mapper", "oidc-usermodel-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-user-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper" ] + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-full-name-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-usermodel-property-mapper", "oidc-address-mapper", "saml-role-list-mapper", "oidc-sha256-pairwise-sub-mapper" ] } } ], "org.keycloak.keys.KeyProvider" : [ { @@ -1771,7 +1839,7 @@ "internationalizationEnabled" : false, "supportedLocales" : [ ], "authenticationFlows" : [ { - "id" : "792d8d8f-d309-44c5-beb7-fea91787e081", + "id" : "f5670e73-ebe7-4df8-a412-720db86688a0", "alias" : "Account verification options", "description" : "Method with which to verity the existing account", "providerId" : "basic-flow", @@ -1793,7 +1861,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "70569ef2-3449-4396-9598-bb5923350072", + "id" : "594021be-b169-45e1-af94-f38308239acb", "alias" : "Authentication Options", "description" : "Authentication options.", "providerId" : "basic-flow", @@ -1822,7 +1890,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "131b9d56-8611-4d41-9bf6-5b23f9e6c27f", + "id" : "008b215a-c415-481e-a56e-d916ad7b8be8", "alias" : "Browser - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1844,7 +1912,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "63e6014f-a0b6-4bad-b3a0-4eb6241fe8e2", + "id" : "225f4a5e-3e16-4f96-a74d-106ee2a648a8", "alias" : "Direct Grant - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1866,7 +1934,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "03220669-d897-4024-ae59-44292d1897be", + "id" : "76e0a042-42ee-4968-bc9c-ea99aa5da1e9", "alias" : "First broker login - Conditional OTP", "description" : "Flow to determine if the OTP is required for the authentication", "providerId" : "basic-flow", @@ -1888,7 +1956,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "ed6a42e8-98d1-4d79-a6ac-2ca4ebfb9853", + "id" : "325c7942-4a29-4be9-8b11-18eac7b94576", "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", @@ -1910,7 +1978,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "b3703eae-dec0-4499-8c55-6f2077483941", + "id" : "5ec59354-2513-4969-94c4-155a5d9d40bb", "alias" : "Reset - Conditional OTP", "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", "providerId" : "basic-flow", @@ -1932,7 +2000,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "8fa9ba51-d2d9-4f4c-96d5-f318753eab5e", + "id" : "f9059e44-8678-4239-b022-964d0778b2eb", "alias" : "User creation or linking", "description" : "Flow for the existing/non-existing user alternatives", "providerId" : "basic-flow", @@ -1955,7 +2023,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "66adeda3-1206-4483-a8f1-3e1541573f4e", + "id" : "aeb11709-cfc9-4983-a49a-a6a394738390", "alias" : "Verify Existing Account by Re-authentication", "description" : "Reauthentication of existing account", "providerId" : "basic-flow", @@ -1977,7 +2045,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "a3e2950d-32d3-4fdd-b110-3eb0de425bd4", + "id" : "9060a17e-e895-4080-8d15-736ffc935d69", "alias" : "browser", "description" : "browser based authentication", "providerId" : "basic-flow", @@ -2013,7 +2081,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "33617d26-0322-4a35-8e48-1f3ffca7a8d4", + "id" : "cf5dd646-4767-4f1f-a868-932f25158e8e", "alias" : "clients", "description" : "Base authentication for clients", "providerId" : "client-flow", @@ -2049,7 +2117,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "04b15af3-99ae-404a-a844-06eb0444d2c4", + "id" : "bd5f02ab-595b-42ef-bee5-536240a1f5e4", "alias" : "direct grant", "description" : "OpenID Connect Resource Owner Grant", "providerId" : "basic-flow", @@ -2078,7 +2146,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "f9499050-d69b-4fd0-8b22-81926234bea2", + "id" : "bb4fc7b4-ce03-48a5-953a-efa64f9b1f08", "alias" : "docker auth", "description" : "Used by Docker clients to authenticate against the IDP", "providerId" : "basic-flow", @@ -2093,7 +2161,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "37b7eaef-6460-47c1-80da-b97213e4fea6", + "id" : "5263fbcf-1e5e-4073-82b9-61c284be6a1d", "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", @@ -2116,7 +2184,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "faf95ea8-ba5a-4c1e-b4ef-b748052b8131", + "id" : "e3b9c110-2293-450c-95a8-1ab67a13a40b", "alias" : "forms", "description" : "Username, password, otp and other auth forms.", "providerId" : "basic-flow", @@ -2138,7 +2206,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "565c209d-bba7-49c8-b1c5-78c3c4284d40", + "id" : "40c9d62a-783d-4434-9642-1c34c9101d87", "alias" : "http challenge", "description" : "An authentication flow based on challenge-response HTTP Authentication Schemes", "providerId" : "basic-flow", @@ -2160,7 +2228,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "1bbee072-9026-4d94-9c63-2694fdb8b2b0", + "id" : "aaecb234-2351-467f-81ad-d56c1f8811a4", "alias" : "registration", "description" : "registration flow", "providerId" : "basic-flow", @@ -2176,7 +2244,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "6f7fa9eb-a4de-4fdf-a052-93ec33706e02", + "id" : "6c79d6c6-08e9-44b1-8d98-e536c18e1b2a", "alias" : "registration form", "description" : "registration form", "providerId" : "form-flow", @@ -2212,7 +2280,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "41f57c84-4db4-4665-ac74-fc0683626d08", + "id" : "1e6cf937-d987-4898-a1dc-765bbae4da72", "alias" : "reset credentials", "description" : "Reset credentials for a user if they forgot their password or something", "providerId" : "basic-flow", @@ -2248,7 +2316,7 @@ "userSetupAllowed" : false } ] }, { - "id" : "951bb395-6a09-4fb9-b688-7a531d68a34b", + "id" : "ea9c6ea6-5cef-4e5c-9ca1-217d2833257b", "alias" : "saml ecp", "description" : "SAML ECP Profile Authentication Flow", "providerId" : "basic-flow", @@ -2264,13 +2332,13 @@ } ] } ], "authenticatorConfig" : [ { - "id" : "349cab01-873e-4aa0-bdc3-50e20e6990cd", + "id" : "8aabe328-f9c8-4842-a084-601deacd79e2", "alias" : "create unique user config", "config" : { "require.password.update.after.registration" : "false" } }, { - "id" : "b2c7f5f4-028b-40df-8dc8-0686736b71a4", + "id" : "398beafd-0718-4a86-a6d4-5a5ab54c8bc6", "alias" : "review profile config", "config" : { "update.profile.on.first.login" : "missing" diff --git a/fda-broker-service/rabbitmq.conf b/fda-broker-service/rabbitmq.conf index f3779fab7f..170b8b8fd2 100644 --- a/fda-broker-service/rabbitmq.conf +++ b/fda-broker-service/rabbitmq.conf @@ -12,7 +12,7 @@ listeners.tcp.1 = 0.0.0.0:5672 # logging log.console = true -log.console.level = debug +log.console.level = warning # Obviously your authentication server cannot vouch for itself, so you'll need another backend with at least one user in # it. You should probably use the internal database diff --git a/fda-container-service/Dockerfile b/fda-container-service/Dockerfile index 0ea94f5755..cac8dff13d 100644 --- a/fda-container-service/Dockerfile +++ b/fda-container-service/Dockerfile @@ -29,8 +29,10 @@ ENV BROKER_PASSWORD=fda ENV SHARED_FILESYSTEM=/tmp ENV USER_NETWORK=userdb ENV LOG_LEVEL=debug -ENV CLIENT_SECRET=client-secret +ENV CLIENT_SECRET="${DBREPO_CLIENT_SECRET:-client-secret}" ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key COPY ./service_ready /usr/bin RUN chmod +x /usr/bin/service_ready diff --git a/fda-container-service/pom.xml b/fda-container-service/pom.xml index 8e9b626ec9..6e0ef2e8cf 100644 --- a/fda-container-service/pom.xml +++ b/fda-container-service/pom.xml @@ -29,6 +29,7 @@ <swagger.version>2.2.2</swagger.version> <jacoco.version>0.8.7</jacoco.version> <sqlserver.version>9.2.1.jre11</sqlserver.version> + <jwt.version>4.3.0</jwt.version> </properties> <dependencies> @@ -62,6 +63,12 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- Monitoring --> <dependency> <groupId>io.micrometer</groupId> diff --git a/fda-container-service/rest-service/src/main/resources/application-docker.yml b/fda-container-service/rest-service/src/main/resources/application-docker.yml index 83273c8f26..f2afff130e 100644 --- a/fda-container-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-container-service/rest-service/src/main/resources/application-docker.yml @@ -38,6 +38,9 @@ fda: network: userdb mount.path: /tmp ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" client_secret: "${CLIENT_SECRET}" client_id: "${CLIENT_ID}" gateway.endpoint: http://gateway-service:9095 \ No newline at end of file diff --git a/fda-container-service/rest-service/src/main/resources/application-local.yml b/fda-container-service/rest-service/src/main/resources/application-local.yml index 2a14576f35..a8ff84d143 100644 --- a/fda-container-service/rest-service/src/main/resources/application-local.yml +++ b/fda-container-service/rest-service/src/main/resources/application-local.yml @@ -38,6 +38,9 @@ fda: network: userdb mount.path: /tmp ready.path: ./ready - client_secret: Gp9IALXWsfftK8ek1J6jNT9hNfWV5U5c + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB + client_secret: client-secret client_id: dbrepo-client gateway.endpoint: http://localhost:9095 \ No newline at end of file diff --git a/fda-container-service/rest-service/src/main/resources/application.yml b/fda-container-service/rest-service/src/main/resources/application.yml index c51eb9dd40..0d80243df8 100644 --- a/fda-container-service/rest-service/src/main/resources/application.yml +++ b/fda-container-service/rest-service/src/main/resources/application.yml @@ -38,6 +38,9 @@ fda: network: "${USER_NETWORK}" mount.path: "${SHARED_FILESYSTEM}" ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" client_secret: "${CLIENT_SECRET}" client_id: "${CLIENT_ID}" gateway.endpoint: http://gateway-service:9095 \ No newline at end of file diff --git a/fda-container-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java b/fda-container-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java index 79e4d7ee81..8dd394b17d 100644 --- a/fda-container-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java +++ b/fda-container-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java @@ -3,7 +3,6 @@ package at.tuwien.auth; import at.tuwien.BaseUnitTest; import at.tuwien.config.H2Utils; import at.tuwien.config.ReadyConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; import at.tuwien.repository.jpa.UserRepository; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; @@ -23,8 +22,6 @@ import java.io.IOException; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @Log4j2 @@ -38,9 +35,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { @MockBean private UserRepository userRepository; - @MockBean - private AuthenticationServiceGateway authenticationServiceGateway; - @Autowired private AuthTokenFilter authTokenFilter; @@ -60,9 +54,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - doThrow(new ServletException("Username not found")) - .when(authenticationServiceGateway) - .validate(anyString()); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.empty()); @@ -80,8 +71,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - when(authenticationServiceGateway.validate(anyString())) - .thenReturn(USER_1_DETAILS); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.of(USER_1)); diff --git a/fda-container-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java b/fda-container-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java deleted file mode 100644 index 09afe818e3..0000000000 --- a/fda-container-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.BaseUnitTest; -import at.tuwien.api.user.UserDto; -import at.tuwien.config.ReadyConfig; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.RestTemplate; - - -import javax.servlet.ServletException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.eq; -import static org.mockito.Mockito.any; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -public class AuthenticationServiceGatewayTest extends BaseUnitTest { - - @MockBean - private ReadyConfig readyConfig; - - @MockBean - private RestTemplate restTemplate; - - @Autowired - private AuthenticationServiceGateway authenticationServiceGateway; - - @Test - public void validate_succeeds() throws ServletException { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.ACCEPTED) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - final UserDetails response = authenticationServiceGateway.validate(JWT_1); - assertNotNull(response); - assertEquals(USER_1_USERNAME, response.getUsername()); - assertEquals(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER")), response.getAuthorities()); - } - - @Test - public void validate_notFound_fails() { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - assertThrows(ServletException.class, () -> { - authenticationServiceGateway.validate(JWT_1); - }); - } - -} diff --git a/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 20b5fd9fcd..b2b01c42ee 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-container-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,8 +1,15 @@ package at.tuwien.auth; -import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,22 +21,30 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; @Slf4j public class AuthTokenFilter extends OncePerRequestFilter { - private final AuthenticationServiceGateway authenticationServiceGateway; + @Value("${fda.jwt.issuer}") + private String issuer; - public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } + @Value("${fda.jwt.public_key}") + private String publicKey; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null) { - final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UserDetails userDetails = verifyJwt(jwt); log.debug("authenticated user {}", userDetails); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -40,6 +55,35 @@ public class AuthTokenFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + /** * Parses the token from the HTTP header of the request * diff --git a/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index d5c8acfb2d..5724f37ead 100644 --- a/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-container-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,10 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; -import at.tuwien.gateway.AuthenticationServiceGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,16 +29,9 @@ import javax.servlet.http.HttpServletResponse; ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final AuthenticationServiceGateway authenticationServiceGateway; - - @Autowired - public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } - @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(authenticationServiceGateway); + return new AuthTokenFilter(); } @Override diff --git a/fda-container-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-container-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java deleted file mode 100644 index 323608b39f..0000000000 --- a/fda-container-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java +++ /dev/null @@ -1,16 +0,0 @@ -package at.tuwien.gateway; - -import org.springframework.security.core.userdetails.UserDetails; - -import javax.servlet.ServletException; - -public interface AuthenticationServiceGateway { - - /** - * Validates a token - * - * @param token The token - * @return User details on success - */ - UserDetails validate(String token) throws ServletException; -} diff --git a/fda-container-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-container-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java deleted file mode 100644 index f2e5e35219..0000000000 --- a/fda-container-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ /dev/null @@ -1,64 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.auth.TokenIntrospectDto; -import at.tuwien.api.user.UserBriefDto; -import at.tuwien.api.user.UserDetailsDto; -import at.tuwien.config.GatewayConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; -import at.tuwien.mapper.UserMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.*; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; - -@Slf4j -@Service -public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { - - private final UserMapper userMapper; - private final RestTemplate restTemplate; - private final GatewayConfig gatewayConfig; - - @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate, GatewayConfig gatewayConfig) { - this.userMapper = userMapper; - this.restTemplate = restTemplate; - this.gatewayConfig = gatewayConfig; - } - - @Override - public UserDetails validate(String token) throws ServletException { - final HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - final MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); - body.add("client_secret", gatewayConfig.getClientSecret()); - body.add("client_id", gatewayConfig.getClientId()); - body.add("token", token); - try { - final ResponseEntity<TokenIntrospectDto> response = restTemplate.exchange("/api/auth/realms/dbrepo/protocol/openid-connect/token/introspect", HttpMethod.POST, - new HttpEntity<>(body, headers), TokenIntrospectDto.class); - if (!response.getStatusCode().equals(HttpStatus.OK)) { - log.error("Failed to validate token with status code {}", response.getStatusCode()); - throw new ServletException("Failed to validate token: http status code is not ok"); - } else if (response.getBody() == null) { - throw new ServletException("Failed to validate token: body is null"); - } else if (!response.getBody().getActive()) { - throw new ServletException("Failed to validate token: token is not active"); - } - final UserDetailsDto dto = userMapper.tokenIntrospectDtoToUserDetailsDto(response.getBody()); - log.trace("gateway authenticated user {}", dto); - return dto; - } catch (HttpStatusCodeException e) { - log.error("Failed to validate token with status code {}", e.getStatusCode()); - throw new ServletException("Failed to validate token", e); - } - } - -} diff --git a/fda-database-service/Dockerfile b/fda-database-service/Dockerfile index d57c8704f0..abc7709426 100644 --- a/fda-database-service/Dockerfile +++ b/fda-database-service/Dockerfile @@ -31,8 +31,10 @@ ENV SEARCH_USERNAME=elastic ENV SEARCH_PASSWORD=elastic ENV GATEWAY_ENDPOINT=http://gateway-service:9095 ENV LOG_LEVEL=debug -ENV CLIENT_SECRET=client-secret +ENV CLIENT_SECRET="${DBREPO_CLIENT_SECRET:-client-secret}" ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key COPY ./service_ready /usr/bin RUN chmod +x /usr/bin/service_ready diff --git a/fda-database-service/pom.xml b/fda-database-service/pom.xml index 84b065b55e..38ddd1951e 100644 --- a/fda-database-service/pom.xml +++ b/fda-database-service/pom.xml @@ -31,6 +31,7 @@ <springfox.version>3.0.0</springfox.version> <jacoco.version>0.8.7</jacoco.version> <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version> + <jwt.version>4.3.0</jwt.version> </properties> <dependencies> @@ -59,6 +60,12 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- Entities and API --> <dependency> <groupId>at.tuwien</groupId> diff --git a/fda-database-service/rest-service/src/main/resources/application-docker.yml b/fda-database-service/rest-service/src/main/resources/application-docker.yml index 10adc604b2..077cd6cfaa 100644 --- a/fda-database-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-database-service/rest-service/src/main/resources/application-docker.yml @@ -43,6 +43,9 @@ fda: username: elastic password: "${ELASTIC_PASSWORD}" ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" client_secret: "${CLIENT_SECRET}" client_id: "${CLIENT_ID}" gateway.endpoint: "${GATEWAY_ENDPOINT}" \ No newline at end of file diff --git a/fda-database-service/rest-service/src/main/resources/application-local.yml b/fda-database-service/rest-service/src/main/resources/application-local.yml index 5ace5e75c2..bd021238de 100644 --- a/fda-database-service/rest-service/src/main/resources/application-local.yml +++ b/fda-database-service/rest-service/src/main/resources/application-local.yml @@ -43,6 +43,9 @@ fda: username: elastic password: elastic ready.path: ./ready - client_secret: Gp9IALXWsfftK8ek1J6jNT9hNfWV5U5c + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB + client_secret: client-secret client_id: dbrepo-client gateway.endpoint: http://localhost:9095 \ No newline at end of file diff --git a/fda-database-service/rest-service/src/main/resources/application.yml b/fda-database-service/rest-service/src/main/resources/application.yml index 9ba93e2013..8a670dbd08 100644 --- a/fda-database-service/rest-service/src/main/resources/application.yml +++ b/fda-database-service/rest-service/src/main/resources/application.yml @@ -43,6 +43,9 @@ fda: username: elastic password: "${ELASTIC_PASSWORD}" ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" client_secret: "${CLIENT_SECRET}" client_id: "${CLIENT_ID}" gateway.endpoint: http://gateway-service:9095 \ No newline at end of file diff --git a/fda-database-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java b/fda-database-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java index c2026c288b..6fa1b5a7ad 100644 --- a/fda-database-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java +++ b/fda-database-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java @@ -4,7 +4,6 @@ import at.tuwien.BaseUnitTest; import at.tuwien.config.H2Utils; import at.tuwien.config.IndexConfig; import at.tuwien.config.ReadyConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; import at.tuwien.repository.jpa.UserRepository; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; @@ -46,9 +45,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { @MockBean private UserRepository userRepository; - @MockBean - private AuthenticationServiceGateway authenticationServiceGateway; - @Autowired private AuthTokenFilter authTokenFilter; @@ -68,9 +64,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - doThrow(new ServletException("Username not found")) - .when(authenticationServiceGateway) - .validate(anyString()); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.empty()); @@ -88,8 +81,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - when(authenticationServiceGateway.validate(anyString())) - .thenReturn(USER_1_DETAILS); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.of(USER_1)); diff --git a/fda-database-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java b/fda-database-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java deleted file mode 100644 index 0abdc13f90..0000000000 --- a/fda-database-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java +++ /dev/null @@ -1,87 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.BaseUnitTest; -import at.tuwien.api.user.UserDto; -import at.tuwien.config.IndexConfig; -import at.tuwien.config.ReadyConfig; -import at.tuwien.repository.jpa.UserRepository; -import com.rabbitmq.client.Channel; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -public class AuthenticationServiceGatewayTest extends BaseUnitTest { - - @MockBean - private ReadyConfig readyConfig; - - @MockBean - private Channel channel; - - @MockBean - private IndexConfig indexConfig; - - @MockBean - @Qualifier("authenticationRestTemplate") - private RestTemplate restTemplate; - - @MockBean - private UserRepository userRepository; - - @Autowired - private AuthenticationServiceGateway authenticationServiceGateway; - - @Test - public void validate_succeeds() throws ServletException { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.ACCEPTED) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - final UserDetails response = authenticationServiceGateway.validate(JWT_1); - assertNotNull(response); - assertEquals(USER_1_USERNAME, response.getUsername()); - assertEquals(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER")), response.getAuthorities()); - } - - @Test - public void validate_notFound_fails() { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - assertThrows(ServletException.class, () -> { - authenticationServiceGateway.validate(JWT_1); - }); - } - -} diff --git a/fda-database-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-database-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 1b7fcb36ea..b2b01c42ee 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-database-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,8 +1,15 @@ package at.tuwien.auth; -import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,22 +21,30 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; @Slf4j public class AuthTokenFilter extends OncePerRequestFilter { - private final AuthenticationServiceGateway authenticationServiceGateway; + @Value("${fda.jwt.issuer}") + private String issuer; - public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } + @Value("${fda.jwt.public_key}") + private String publicKey; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null) { - final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UserDetails userDetails = verifyJwt(jwt); log.debug("authenticated user {}", userDetails); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -40,17 +55,46 @@ public class AuthTokenFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + /** * Parses the token from the HTTP header of the request * * @param request The request. * @return The token. */ - String parseJwt(HttpServletRequest request) { + public String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7, headerAuth.length()); } return null; } -} +} \ No newline at end of file diff --git a/fda-database-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-database-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 1bcc47efa7..afff7f2dad 100644 --- a/fda-database-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-database-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,10 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; -import at.tuwien.gateway.AuthenticationServiceGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,16 +29,9 @@ import javax.servlet.http.HttpServletResponse; ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final AuthenticationServiceGateway authenticationServiceGateway; - - @Autowired - public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } - @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(authenticationServiceGateway); + return new AuthTokenFilter(); } @Override diff --git a/fda-database-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-database-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java deleted file mode 100644 index 323608b39f..0000000000 --- a/fda-database-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java +++ /dev/null @@ -1,16 +0,0 @@ -package at.tuwien.gateway; - -import org.springframework.security.core.userdetails.UserDetails; - -import javax.servlet.ServletException; - -public interface AuthenticationServiceGateway { - - /** - * Validates a token - * - * @param token The token - * @return User details on success - */ - UserDetails validate(String token) throws ServletException; -} diff --git a/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java deleted file mode 100644 index dbdec859da..0000000000 --- a/fda-database-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ /dev/null @@ -1,66 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.auth.TokenIntrospectDto; -import at.tuwien.api.user.UserDetailsDto; -import at.tuwien.config.GatewayConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; -import at.tuwien.mapper.UserMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.http.*; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; -import org.springframework.util.LinkedMultiValueMap; -import org.springframework.util.MultiValueMap; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; - -@Slf4j -@Service -public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { - - private final UserMapper userMapper; - private final RestTemplate restTemplate; - private final GatewayConfig gatewayConfig; - - @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, - @Qualifier("authenticationRestTemplate") RestTemplate restTemplate, - GatewayConfig gatewayConfig) { - this.userMapper = userMapper; - this.restTemplate = restTemplate; - this.gatewayConfig = gatewayConfig; - } - - @Override - public UserDetails validate(String token) throws ServletException { - final HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); - final MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); - body.add("client_secret", gatewayConfig.getClientSecret()); - body.add("client_id", gatewayConfig.getClientId()); - body.add("token", token); - try { - final ResponseEntity<TokenIntrospectDto> response = restTemplate.exchange("/api/auth/realms/dbrepo/protocol/openid-connect/token/introspect", HttpMethod.POST, - new HttpEntity<>(body, headers), TokenIntrospectDto.class); - if (!response.getStatusCode().equals(HttpStatus.OK)) { - log.error("Failed to validate token with status code {}", response.getStatusCode()); - throw new ServletException("Failed to validate token: http status code is not ok"); - } else if (response.getBody() == null) { - throw new ServletException("Failed to validate token: body is null"); - } else if (!response.getBody().getActive()) { - throw new ServletException("Failed to validate token: token is not active"); - } - final UserDetailsDto dto = userMapper.tokenIntrospectDtoToUserDetailsDto(response.getBody()); - log.trace("gateway authenticated user {}", dto); - return dto; - } catch (HttpStatusCodeException e) { - log.error("Failed to validate token with status code {}", e.getStatusCode()); - throw new ServletException("Failed to validate token", e); - } - } - -} diff --git a/fda-identifier-service/Dockerfile b/fda-identifier-service/Dockerfile index d9976484e8..47cbf34ae5 100644 --- a/fda-identifier-service/Dockerfile +++ b/fda-identifier-service/Dockerfile @@ -28,6 +28,10 @@ ENV METADATA_PASSWORD=dbrepo ENV GATEWAY_ENDPOINT=http://gateway-service:9095 ENV WEBSITE=http://localhost:3000 ENV LOG_LEVEL=debug +ENV CLIENT_SECRET="${DBREPO_CLIENT_SECRET:-client-secret}" +ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key COPY ./service_ready /usr/bin RUN chmod +x /usr/bin/service_ready diff --git a/fda-identifier-service/pom.xml b/fda-identifier-service/pom.xml index b5fbe58b6a..1412057044 100644 --- a/fda-identifier-service/pom.xml +++ b/fda-identifier-service/pom.xml @@ -33,6 +33,7 @@ <maven-site.version>3.10.0</maven-site.version> <docker.version>3.2.7</docker.version> <commons-io.version>2.11.0</commons-io.version> + <jwt.version>4.3.0</jwt.version> </properties> <dependencies> @@ -70,6 +71,12 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- Monitoring --> <dependency> <groupId>io.micrometer</groupId> diff --git a/fda-identifier-service/rest-service/src/main/resources/application-docker.yml b/fda-identifier-service/rest-service/src/main/resources/application-docker.yml index 418f38ddab..1930d3a318 100644 --- a/fda-identifier-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-identifier-service/rest-service/src/main/resources/application-docker.yml @@ -35,6 +35,11 @@ eureka: client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ fda: ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: "${GATEWAY_ENDPOINT}" website: "${WEBSITE}" elastic: diff --git a/fda-identifier-service/rest-service/src/main/resources/application-local.yml b/fda-identifier-service/rest-service/src/main/resources/application-local.yml index d40853659b..6a7ec7b454 100644 --- a/fda-identifier-service/rest-service/src/main/resources/application-local.yml +++ b/fda-identifier-service/rest-service/src/main/resources/application-local.yml @@ -35,6 +35,11 @@ eureka: client.serviceUrl.defaultZone: http://localhost:9090/eureka/ fda: ready.path: ./ready + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB + client_secret: client-secret + client_id: dbrepo-client gateway.endpoint: http://localhost:9095 website: http://localhost:3000 elastic: diff --git a/fda-identifier-service/rest-service/src/main/resources/application.yml b/fda-identifier-service/rest-service/src/main/resources/application.yml index e0c7ffd06f..c31a5533d2 100644 --- a/fda-identifier-service/rest-service/src/main/resources/application.yml +++ b/fda-identifier-service/rest-service/src/main/resources/application.yml @@ -35,6 +35,11 @@ eureka: client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ fda: ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: "${GATEWAY_ENDPOINT}" website: "${WEBSITE}" elastic: diff --git a/fda-identifier-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java b/fda-identifier-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java index b7dc8495cd..b18801abc8 100644 --- a/fda-identifier-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java +++ b/fda-identifier-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java @@ -4,7 +4,6 @@ import at.tuwien.BaseUnitTest; import at.tuwien.config.H2Utils; import at.tuwien.config.IndexInitializer; import at.tuwien.config.ReadyConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; import at.tuwien.repository.jpa.UserRepository; import lombok.extern.log4j.Log4j2; import org.junit.jupiter.api.BeforeEach; @@ -24,8 +23,6 @@ import java.io.IOException; import java.util.Optional; import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.when; @Log4j2 @@ -42,9 +39,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { @MockBean private UserRepository userRepository; - @MockBean - private AuthenticationServiceGateway authenticationServiceGateway; - @Autowired private AuthTokenFilter authTokenFilter; @@ -64,9 +58,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - doThrow(new ServletException("Username not found")) - .when(authenticationServiceGateway) - .validate(anyString()); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.empty()); @@ -84,8 +75,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - when(authenticationServiceGateway.validate(anyString())) - .thenReturn(USER_1_DETAILS); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.of(USER_1)); diff --git a/fda-identifier-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java b/fda-identifier-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java deleted file mode 100644 index d3ed4bc47e..0000000000 --- a/fda-identifier-service/rest-service/src/test/java/at/tuwien/gateway/AuthenticationServiceGatewayTest.java +++ /dev/null @@ -1,81 +0,0 @@ -package at.tuwien.gateway; - -import at.tuwien.BaseUnitTest; -import at.tuwien.api.user.UserDto; -import at.tuwien.config.IndexInitializer; -import at.tuwien.config.ReadyConfig; -import at.tuwien.repository.jpa.UserRepository; -import lombok.extern.log4j.Log4j2; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.test.context.junit.jupiter.SpringExtension; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.*; - -@Log4j2 -@SpringBootTest -@ExtendWith(SpringExtension.class) -public class AuthenticationServiceGatewayTest extends BaseUnitTest { - - @MockBean - private IndexInitializer indexInitializer; - - @MockBean - private ReadyConfig readyConfig; - - @MockBean - private RestTemplate restTemplate; - - @MockBean - private UserRepository userRepository; - - @Autowired - private AuthenticationServiceGateway authenticationServiceGateway; - - @Test - public void validate_succeeds() throws ServletException { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.ACCEPTED) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - final UserDetails response = authenticationServiceGateway.validate(JWT_1); - assertNotNull(response); - assertEquals(USER_1_USERNAME, response.getUsername()); - assertEquals(List.of(new SimpleGrantedAuthority("ROLE_RESEARCHER")), response.getAuthorities()); - } - - @Test - public void validate_notFound_fails() { - final ResponseEntity<UserDto> mock = ResponseEntity.status(HttpStatus.NOT_FOUND) - .body(USER_1_DTO); - - /* mock */ - when(restTemplate.exchange(anyString(), eq(HttpMethod.PUT), any(HttpEntity.class), eq(UserDto.class))) - .thenReturn(mock); - - /* test */ - assertThrows(ServletException.class, () -> { - authenticationServiceGateway.validate(JWT_1); - }); - } - -} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 1b7fcb36ea..b2b01c42ee 100644 --- a/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-identifier-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,8 +1,15 @@ package at.tuwien.auth; -import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,22 +21,30 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; @Slf4j public class AuthTokenFilter extends OncePerRequestFilter { - private final AuthenticationServiceGateway authenticationServiceGateway; + @Value("${fda.jwt.issuer}") + private String issuer; - public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } + @Value("${fda.jwt.public_key}") + private String publicKey; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null) { - final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UserDetails userDetails = verifyJwt(jwt); log.debug("authenticated user {}", userDetails); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -40,17 +55,46 @@ public class AuthTokenFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + /** * Parses the token from the HTTP header of the request * * @param request The request. * @return The token. */ - String parseJwt(HttpServletRequest request) { + public String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7, headerAuth.length()); } return null; } -} +} \ No newline at end of file diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index d665ebbbd4..04f19f375b 100644 --- a/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-identifier-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,10 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; -import at.tuwien.gateway.AuthenticationServiceGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,16 +29,9 @@ import javax.servlet.http.HttpServletResponse; ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final AuthenticationServiceGateway authenticationServiceGateway; - - @Autowired - public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } - @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(authenticationServiceGateway); + return new AuthTokenFilter(); } @Override diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java deleted file mode 100644 index 7eab6f1f8d..0000000000 --- a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java +++ /dev/null @@ -1,17 +0,0 @@ -package at.tuwien.gateway; - -import org.springframework.security.core.userdetails.UserDetails; - -import javax.servlet.ServletException; - -public interface AuthenticationServiceGateway { - - /** - * Validates a token - * - * @param token The token - * @return User details on success - * @throws ServletException The token failed to validate at the Authentication Service. - */ - UserDetails validate(String token) throws ServletException; -} diff --git a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java deleted file mode 100644 index 65a1b38867..0000000000 --- a/fda-identifier-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.user.UserDto; -import at.tuwien.gateway.AuthenticationServiceGateway; -import at.tuwien.mapper.UserMapper; -import lombok.extern.log4j.Log4j2; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.*; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; - -@Log4j2 -@Service -public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { - - private final UserMapper userMapper; - private final RestTemplate restTemplate; - - @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { - this.userMapper = userMapper; - this.restTemplate = restTemplate; - } - - @Override - public UserDetails validate(String token) throws ServletException { - final HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - try { - final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, - new HttpEntity<>(null, headers), UserDto.class); - if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { - log.error("Failed to validate token with status code {}", response.getStatusCode()); - throw new ServletException("Failed to validate token"); - } - final UserDetails dto = userMapper.userDtoToUserDetailsDto(response.getBody()); - log.trace("gateway authenticated user {}", dto); - return dto; - } catch (HttpStatusCodeException e) { - log.error("Failed to validate token with status code {}", e.getStatusCode()); - throw new ServletException("Failed to validate token", e); - } - } - -} diff --git a/fda-query-service/Dockerfile b/fda-query-service/Dockerfile index 7bcbda154f..90a92f23bd 100644 --- a/fda-query-service/Dockerfile +++ b/fda-query-service/Dockerfile @@ -31,6 +31,10 @@ ENV GATEWAY_ENDPOINT=http://gateway-service:9095 ENV SHARED_FILESYSTEM=/tmp ENV BROKER_CONSUMERS=2 ENV LOG_LEVEL=debug +ENV CLIENT_SECRET="${DBREPO_CLIENT_SECRET:-client-secret}" +ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key ENV NOT_SUPPORTED_KEYWORDS=\\*,AVG,BIT_AND,BIT_OR,BIT_XOR,COUNT,COUNTDISTINCT,GROUP_CONCAT,JSON_ARRAYAGG,JSON_OBJECTAGG,MAX,MIN,STD,STDDEV,STDDEV_POP,STDDEV_SAMP,SUM,VARIANCE,VAR_POP,VAR_SAMP,-- COPY ./service_ready /usr/bin diff --git a/fda-query-service/pom.xml b/fda-query-service/pom.xml index 9f83eafd6d..67fb838c96 100644 --- a/fda-query-service/pom.xml +++ b/fda-query-service/pom.xml @@ -51,6 +51,7 @@ <opencsv.version>5.4</opencsv.version> <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version> <maven-report.version>3.0.0</maven-report.version> + <jwt.version>4.3.0</jwt.version> </properties> <dependencies> @@ -96,6 +97,12 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- Monitoring --> <dependency> <groupId>io.micrometer</groupId> diff --git a/fda-query-service/rest-service/src/main/resources/application-docker.yml b/fda-query-service/rest-service/src/main/resources/application-docker.yml index 4eb5d1238b..0010b04b70 100644 --- a/fda-query-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-query-service/rest-service/src/main/resources/application-docker.yml @@ -40,6 +40,11 @@ eureka: fda: gateway.endpoint: "${GATEWAY_ENDPOINT}" ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" consumers: 2 unsupported: "${NOT_SUPPORTED_KEYWORDS}" elastic: diff --git a/fda-query-service/rest-service/src/main/resources/application-local.yml b/fda-query-service/rest-service/src/main/resources/application-local.yml index 9451262fbd..2efc331ed9 100644 --- a/fda-query-service/rest-service/src/main/resources/application-local.yml +++ b/fda-query-service/rest-service/src/main/resources/application-local.yml @@ -40,6 +40,11 @@ eureka: fda: gateway.endpoint: http://localhost:9095 ready.path: ./ready + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB + client_secret: client-secret + client_id: dbrepo-client consumers: 2 unsupported: \*,AVG,BIT_AND,BIT_OR,BIT_XOR,COUNT,COUNTDISTINCT,GROUP_CONCAT,JSON_ARRAYAGG,JSON_OBJECTAGG,MAX,MIN,STD,STDDEV,STDDEV_POP,STDDEV_SAMP,SUM,VARIANCE,VAR_POP,VAR_SAMP,-- elastic: diff --git a/fda-query-service/rest-service/src/main/resources/application.yml b/fda-query-service/rest-service/src/main/resources/application.yml index 3821bd12af..f773caffc0 100644 --- a/fda-query-service/rest-service/src/main/resources/application.yml +++ b/fda-query-service/rest-service/src/main/resources/application.yml @@ -40,6 +40,11 @@ eureka: fda: gateway.endpoint: "${GATEWAY_ENDPOINT}" ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" consumers: "${BROKER_CONSUMERS}" unsupported: "${NOT_SUPPORTED_KEYWORDS}" elastic: diff --git a/fda-query-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java b/fda-query-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java index bf3df721ba..8b0d8be0ce 100644 --- a/fda-query-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java +++ b/fda-query-service/rest-service/src/test/java/at/tuwien/auth/AuthTokenFilterTest.java @@ -4,7 +4,6 @@ import at.tuwien.BaseUnitTest; import at.tuwien.config.H2Utils; import at.tuwien.config.IndexConfig; import at.tuwien.config.ReadyConfig; -import at.tuwien.gateway.AuthenticationServiceGateway; import at.tuwien.repository.jpa.UserRepository; import com.rabbitmq.client.Channel; import lombok.extern.log4j.Log4j2; @@ -45,9 +44,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { @MockBean private UserRepository userRepository; - @MockBean - private AuthenticationServiceGateway authenticationServiceGateway; - @Autowired private AuthTokenFilter authTokenFilter; @@ -62,9 +58,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - doThrow(new ServletException("Username not found")) - .when(authenticationServiceGateway) - .validate(anyString()); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.empty()); @@ -82,8 +75,6 @@ public class AuthTokenFilterTest extends BaseUnitTest { final FilterChain chain = new MockFilterChain(); /* mock */ - when(authenticationServiceGateway.validate(anyString())) - .thenReturn(USER_1_DETAILS); when(userRepository.findByUsername("mweise")) .thenReturn(Optional.of(USER_1)); diff --git a/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 1b7fcb36ea..b2b01c42ee 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-query-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,8 +1,15 @@ package at.tuwien.auth; -import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,22 +21,30 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; @Slf4j public class AuthTokenFilter extends OncePerRequestFilter { - private final AuthenticationServiceGateway authenticationServiceGateway; + @Value("${fda.jwt.issuer}") + private String issuer; - public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } + @Value("${fda.jwt.public_key}") + private String publicKey; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null) { - final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UserDetails userDetails = verifyJwt(jwt); log.debug("authenticated user {}", userDetails); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -40,17 +55,46 @@ public class AuthTokenFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + /** * Parses the token from the HTTP header of the request * * @param request The request. * @return The token. */ - String parseJwt(HttpServletRequest request) { + public String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7, headerAuth.length()); } return null; } -} +} \ No newline at end of file diff --git a/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 47807018bc..22586fb23b 100644 --- a/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-query-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,10 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; -import at.tuwien.gateway.AuthenticationServiceGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,16 +29,9 @@ import javax.servlet.http.HttpServletResponse; ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final AuthenticationServiceGateway authenticationServiceGateway; - - @Autowired - public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } - @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(authenticationServiceGateway); + return new AuthTokenFilter(); } @Override diff --git a/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java deleted file mode 100644 index 323608b39f..0000000000 --- a/fda-query-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java +++ /dev/null @@ -1,16 +0,0 @@ -package at.tuwien.gateway; - -import org.springframework.security.core.userdetails.UserDetails; - -import javax.servlet.ServletException; - -public interface AuthenticationServiceGateway { - - /** - * Validates a token - * - * @param token The token - * @return User details on success - */ - UserDetails validate(String token) throws ServletException; -} diff --git a/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java deleted file mode 100644 index 2a922bc209..0000000000 --- a/fda-query-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ /dev/null @@ -1,49 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.user.UserDto; -import at.tuwien.gateway.AuthenticationServiceGateway; -import at.tuwien.mapper.UserMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.*; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; -import org.springframework.web.client.HttpStatusCodeException; -import org.springframework.web.client.RestTemplate; - -import javax.servlet.ServletException; - -@Slf4j -@Service -public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { - - private final UserMapper userMapper; - private final RestTemplate restTemplate; - - @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { - this.userMapper = userMapper; - this.restTemplate = restTemplate; - } - - @Override - public UserDetails validate(String token) throws ServletException { - final HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - try { - final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, - new HttpEntity<>(null, headers), UserDto.class); - if (!response.getStatusCode().equals(HttpStatus.ACCEPTED)) { - log.error("Failed to validate token with status code {}", response.getStatusCode()); - throw new ServletException("Failed to validate token"); - } - final UserDetails dto = userMapper.userDtoToUserDetailsDto(response.getBody()); - log.trace("gateway authenticated user {}", dto); - return dto; - } catch (HttpStatusCodeException e) { - log.error("Failed to validate token with status code {}", e.getStatusCode()); - throw new ServletException("Failed to validate token", e); - } - } - -} diff --git a/fda-table-service/Dockerfile b/fda-table-service/Dockerfile index 4752a4a140..cac8dff13d 100644 --- a/fda-table-service/Dockerfile +++ b/fda-table-service/Dockerfile @@ -29,6 +29,10 @@ ENV BROKER_PASSWORD=fda ENV SHARED_FILESYSTEM=/tmp ENV USER_NETWORK=userdb ENV LOG_LEVEL=debug +ENV CLIENT_SECRET="${DBREPO_CLIENT_SECRET:-client-secret}" +ENV CLIENT_ID=dbrepo-client +ENV JWT_ISSUER=http://localhost:8080/realms/dbrepo +ENV JWT_PUBKEY=public-key COPY ./service_ready /usr/bin RUN chmod +x /usr/bin/service_ready diff --git a/fda-table-service/pom.xml b/fda-table-service/pom.xml index e9faf0cbb3..aa490cf812 100644 --- a/fda-table-service/pom.xml +++ b/fda-table-service/pom.xml @@ -34,6 +34,7 @@ <random-utils.version>2.0.0</random-utils.version> <hibernate-c3po.version>5.6.3.Final</hibernate-c3po.version> <jsql.version>3.1</jsql.version> + <jwt.version>4.3.0</jwt.version> </properties> <dependencies> @@ -67,6 +68,12 @@ <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> + <!-- Authentication --> + <dependency> + <groupId>com.auth0</groupId> + <artifactId>java-jwt</artifactId> + <version>${jwt.version}</version> + </dependency> <!-- Monitoring --> <dependency> <groupId>io.micrometer</groupId> diff --git a/fda-table-service/rest-service/src/main/resources/application-docker.yml b/fda-table-service/rest-service/src/main/resources/application-docker.yml index 1a03468a50..7e54984965 100644 --- a/fda-table-service/rest-service/src/main/resources/application-docker.yml +++ b/fda-table-service/rest-service/src/main/resources/application-docker.yml @@ -39,6 +39,11 @@ eureka: client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ fda: ready.path: /ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: "${GATEWAY_ENDPOINT}" elastic: endpoint: search-service:9200 diff --git a/fda-table-service/rest-service/src/main/resources/application-local.yml b/fda-table-service/rest-service/src/main/resources/application-local.yml index 1852e935c8..c0fd28220a 100644 --- a/fda-table-service/rest-service/src/main/resources/application-local.yml +++ b/fda-table-service/rest-service/src/main/resources/application-local.yml @@ -39,6 +39,11 @@ eureka: client.serviceUrl.defaultZone: http://localhost:9090/eureka/ fda: ready.path: ./ready + jwt: + issuer: http://localhost:8080/realms/dbrepo + public_key: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqqnHQ2BWWW9vDNLRCcxD++xZg/16oqMo/c1l+lcFEjjAIJjJp/HqrPYU/U9GvquGE6PbVFtTzW1KcKawOW+FJNOA3CGo8Q1TFEfz43B8rZpKsFbJKvQGVv1Z4HaKPvLUm7iMm8Hv91cLduuoWx6Q3DPe2vg13GKKEZe7UFghF+0T9u8EKzA/XqQ0OiICmsmYPbwvf9N3bCKsB/Y10EYmZRb8IhCoV9mmO5TxgWgiuNeCTtNCv2ePYqL/U0WvyGFW0reasIK8eg3KrAUj8DpyOgPOVBn3lBGf+3KFSYi+0bwZbJZWqbC/Xlk20Go1YfeJPRIt7ImxD27R/lNjgDO/MwIDAQAB + client_secret: client-secret + client_id: dbrepo-client gateway.endpoint: http://localhost:9095 elastic: endpoint: search-service:9200 diff --git a/fda-table-service/rest-service/src/main/resources/application.yml b/fda-table-service/rest-service/src/main/resources/application.yml index c0853c68f4..0197bd442b 100644 --- a/fda-table-service/rest-service/src/main/resources/application.yml +++ b/fda-table-service/rest-service/src/main/resources/application.yml @@ -39,6 +39,11 @@ eureka: client.serviceUrl.defaultZone: http://discovery-service:9090/eureka/ fda: ready.path: ./ready + jwt: + issuer: "${JWT_ISSUER}" + public_key: "${JWT_PUBKEY}" + client_secret: "${CLIENT_SECRET}" + client_id: "${CLIENT_ID}" gateway.endpoint: http://gateway-service:9095 elastic: endpoint: search-service:9200 diff --git a/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java b/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java index 12854321a8..b2b01c42ee 100644 --- a/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java +++ b/fda-table-service/services/src/main/java/at/tuwien/auth/AuthTokenFilter.java @@ -1,8 +1,15 @@ package at.tuwien.auth; -import at.tuwien.gateway.AuthenticationServiceGateway; +import at.tuwien.api.auth.RealmAccessDto; +import at.tuwien.api.user.UserDetailsDto; +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.interfaces.DecodedJWT; import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; @@ -14,22 +21,30 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.X509EncodedKeySpec; +import java.util.Arrays; +import java.util.Base64; +import java.util.stream.Collectors; @Slf4j public class AuthTokenFilter extends OncePerRequestFilter { - private final AuthenticationServiceGateway authenticationServiceGateway; + @Value("${fda.jwt.issuer}") + private String issuer; - public AuthTokenFilter(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } + @Value("${fda.jwt.public_key}") + private String publicKey; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String jwt = parseJwt(request); if (jwt != null) { - final UserDetails userDetails = authenticationServiceGateway.validate(jwt); + final UserDetails userDetails = verifyJwt(jwt); log.debug("authenticated user {}", userDetails); final UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); @@ -40,13 +55,42 @@ public class AuthTokenFilter extends OncePerRequestFilter { filterChain.doFilter(request, response); } + public UserDetails verifyJwt(String token) throws ServletException { + final KeyFactory kf; + try { + kf = KeyFactory.getInstance("RSA"); + } catch (NoSuchAlgorithmException e) { + log.error("Failed to find RSA algorithm"); + throw new ServletException("Failed to find RSA algorithm", e); + } + final X509EncodedKeySpec keySpecX509 = new X509EncodedKeySpec(Base64.getDecoder().decode(publicKey)); + final RSAPublicKey pubKey; + try { + pubKey = (RSAPublicKey) kf.generatePublic(keySpecX509); + } catch (InvalidKeySpecException e) { + log.error("Provided public key is invalid"); + throw new ServletException("Provided public key is invalid", e); + } + final Algorithm algorithm = Algorithm.RSA256(pubKey, null); + JWTVerifier verifier = JWT.require(algorithm) + .withIssuer(issuer) + .withAudience("spring") + .build(); + final DecodedJWT jwt = verifier.verify(token); + final RealmAccessDto realmAccess = jwt.getClaim("realm_access").as(RealmAccessDto.class); + return UserDetailsDto.builder() + .username(jwt.getClaim("client_id").asString()) + .authorities(Arrays.stream(realmAccess.getRoles()).map(SimpleGrantedAuthority::new).collect(Collectors.toList())) + .build(); + } + /** * Parses the token from the HTTP header of the request * * @param request The request. * @return The token. */ - private String parseJwt(HttpServletRequest request) { + public String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7, headerAuth.length()); diff --git a/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java b/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java index 1f16894912..be46fcc18b 100644 --- a/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java +++ b/fda-table-service/services/src/main/java/at/tuwien/config/WebSecurityConfig.java @@ -1,10 +1,8 @@ package at.tuwien.config; import at.tuwien.auth.AuthTokenFilter; -import at.tuwien.gateway.AuthenticationServiceGateway; import io.swagger.v3.oas.annotations.enums.SecuritySchemeType; import io.swagger.v3.oas.annotations.security.SecurityScheme; -import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; @@ -31,16 +29,9 @@ import javax.servlet.http.HttpServletResponse; ) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { - private final AuthenticationServiceGateway authenticationServiceGateway; - - @Autowired - public WebSecurityConfig(AuthenticationServiceGateway authenticationServiceGateway) { - this.authenticationServiceGateway = authenticationServiceGateway; - } - @Bean public AuthTokenFilter authTokenFilter() { - return new AuthTokenFilter(authenticationServiceGateway); + return new AuthTokenFilter(); } @Override diff --git a/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java b/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java deleted file mode 100644 index fc47ce246d..0000000000 --- a/fda-table-service/services/src/main/java/at/tuwien/gateway/AuthenticationServiceGateway.java +++ /dev/null @@ -1,14 +0,0 @@ -package at.tuwien.gateway; - -import org.springframework.security.core.userdetails.UserDetails; - -public interface AuthenticationServiceGateway { - - /** - * Validates a token - * - * @param token The token - * @return User details on success - */ - UserDetails validate(String token); -} diff --git a/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java b/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java deleted file mode 100644 index 9b94b6b4f4..0000000000 --- a/fda-table-service/services/src/main/java/at/tuwien/gateway/impl/AuthenticationServiceGatewayImpl.java +++ /dev/null @@ -1,38 +0,0 @@ -package at.tuwien.gateway.impl; - -import at.tuwien.api.user.UserDto; -import at.tuwien.gateway.AuthenticationServiceGateway; -import at.tuwien.mapper.UserMapper; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpEntity; -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.ResponseEntity; -import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.stereotype.Service; -import org.springframework.web.client.RestTemplate; - -@Slf4j -@Service -public class AuthenticationServiceGatewayImpl implements AuthenticationServiceGateway { - - private final UserMapper userMapper; - private final RestTemplate restTemplate; - - @Autowired - public AuthenticationServiceGatewayImpl(UserMapper userMapper, RestTemplate restTemplate) { - this.userMapper = userMapper; - this.restTemplate = restTemplate; - } - - @Override - public UserDetails validate(String token) { - final HttpHeaders headers = new HttpHeaders(); - headers.set("Authorization", "Bearer " + token); - final ResponseEntity<UserDto> response = restTemplate.exchange("/api/auth", HttpMethod.PUT, - new HttpEntity<>(null, headers), UserDto.class); - return userMapper.userDtoToUserDetailsDto(response.getBody()); - } - -} -- GitLab